Compare commits

...
529 Commits
Author SHA1 Message Date
github-actions[bot] fa88d35b2b chore: bump version to 1.34.1 2026-04-20 06:15:51 +00:00
edde746 64d5c1f6aa fix: force rediscover on manual reconnect 2026-04-20 08:15:16 +02:00
github-actions[bot] 56baa48223 chore: bump version to 1.34.0 2026-04-20 06:13:39 +00:00
edde746 ad3edd4652 feat: log connection failure detail 2026-04-19 23:05:22 +02:00
edde746 81bac6ee9b fix(exoplayer): libass performance 2026-04-19 23:01:40 +02:00
edde746 a715f4e26b feat: collection/playlist sync rules 2026-04-19 17:45:03 +02:00
edde746 6115c21b44 chore: prune unused i18n keys 2026-04-19 14:48:18 +02:00
edde746 f367260acd chore: i18n cleanup script 2026-04-19 14:47:52 +02:00
edde746 7f8b827c0e fix: reset completion latch on stream recovery
close #664
2026-04-19 14:08:20 +02:00
edde746 0a1abfcbbb fix: video playback tile description 2026-04-19 13:42:58 +02:00
edde746 f53592f32b fix: live tv dvr seek via keyboard/remote
close #789
2026-04-19 13:13:38 +02:00
edde746 fd1d187ede feat: timeline thumbnail on dpad key-repeat
close #805
2026-04-19 12:24:14 +02:00
edde746 b4e78febc0 feat: donation button 2026-04-19 11:52:46 +02:00
edde746 ff2928b18f feat: trakt integration
close #617
2026-04-19 11:31:27 +02:00
edde746 8b34307975 fix: re-select audio on renderer capabilities change
close #889
2026-04-18 18:36:44 +02:00
edde746 9cca40420c fix: guard player back pop with canPop 2026-04-18 18:08:51 +02:00
edde746 3da51f9d64 chore: pre-commit ci hook, dart format 2026-04-18 12:40:35 +02:00
edde746 2bd9e6655f feat: rate pill, fix snackbar placement
close #886
2026-04-18 08:37:43 +02:00
edde746 d2b01100a5 feat: force TV mode setting
close #885
2026-04-17 19:41:28 +02:00
edde746 47837edb01 fix: apply display matching on fullscreen enter 2026-04-17 15:03:50 +02:00
edde746 91057c12a4 feat: bold/italic subtitle toggles
close #883
2026-04-17 15:03:23 +02:00
edde746 66b32468eb fix: serialize file picker calls 2026-04-17 14:13:20 +02:00
edde746 1e62c6c756 feat: bandwidth limit modal on stream 500 2026-04-17 13:12:07 +02:00
edde746 f8132e7bdb fix: focus media detail info rows
close #758
2026-04-17 05:23:18 +02:00
edde746 dec2e1c9fc fix: add media3 hls dependency
close #881
2026-04-17 04:42:46 +02:00
edde746 ee20965218 fix: pin EPG titles, filter cloud channel list
close #614
2026-04-17 04:28:19 +02:00
edde746 2e429840d2 fix: fan out multi-airing EPG metadata
close #563
2026-04-17 03:36:27 +02:00
edde746 e63920e23b fix: native monitor-aware fullscreen on windows 2026-04-16 23:13:15 +02:00
edde746 11d2e40e34 fix: downsample offline hero art and clear logo 2026-04-16 22:50:58 +02:00
edde746 ddb4a91b08 fix: downsample local artwork in offline grids 2026-04-16 22:44:01 +02:00
edde746 157669bd76 feat: folder view thumbnails and rich metadata
close #821
2026-04-16 17:06:07 +02:00
edde746 f055759c7a fix: suppress position sync when app backgrounded 2026-04-16 15:43:08 +02:00
edde746 34815f22ee fix: watch together server races, reconnect readiness, session end 2026-04-16 15:24:44 +02:00
edde746 1d3ae3e1c5 fix: watch together bugs and server write serialization 2026-04-16 14:00:24 +02:00
edde746 3cf1ebc09b fix: watch together room re-join and participant discovery
close #875
2026-04-16 13:17:24 +02:00
edde746 360741674d feat: sync rules for downloads
close #867
2026-04-16 12:52:48 +02:00
edde746 c369f2c3d7 refactor: remove dead code across codebase 2026-04-16 08:14:42 +02:00
edde746 65e22fbbbd refactor: remove dead download code 2026-04-16 07:27:05 +02:00
edde746 74b2d76fea refactor: remove dead SAF copy methods 2026-04-16 07:27:05 +02:00
edde746 6c61e0e4c9 feat: namedTimeout for Sentry context 2026-04-16 07:27:05 +02:00
edde746 a60e1e7f42 feat: ExoPlayer audio-delay and sub-delay support
close #869
2026-04-16 06:44:46 +02:00
edde746 1b1c4b741b filter Windows disk-full errors from Sentry 2026-04-16 06:25:24 +02:00
edde746 f9d8549e56 fix: clamp tooltip upper bound to prevent ArgumentError 2026-04-16 06:13:45 +02:00
edde746 bdd9d24d32 fix: fallback to IOClient on WinHttp init failure 2026-04-16 06:11:13 +02:00
edde746 a7ba837cb1 fix: prevent white screen when no servers available 2026-04-16 06:02:18 +02:00
edde746 d04781e4b3 fix: use baseline x86_64 mpv build for older CPU compat 2026-04-16 04:19:43 +02:00
edde746 f87f0b0807 fix: suppress spurious media pause during frame rate switch
close #871
2026-04-16 04:11:33 +02:00
edde746 59346f4b67 fix: restore playWhenReady after renderer recreation 2026-04-16 03:42:46 +02:00
edde746 06174af16c fix: ensure crashpad_handler has executable permissions in .deb
close #870
2026-04-15 23:25:00 +02:00
edde746 f1867ddeb7 fix: preserve PiP during episode auto-play
close #865
2026-04-15 23:16:04 +02:00
edde746 79f7de1b4c chore: regenerate podfile locks 2026-04-15 17:53:46 +02:00
edde746 15de88463c fix: disable native HTTP error capturing in sentry 2026-04-15 17:51:28 +02:00
github-actions[bot] c032d97c5f chore: update cask to 1.33.1 2026-04-15 10:20:55 +00:00
github-actions[bot] 2eca47ab69 chore: bump version to 1.33.1 2026-04-15 10:02:27 +00:00
edde746 caa70dd460 fix: remove camera permission and hasCamera check 2026-04-15 12:01:55 +02:00
github-actions[bot] 6b0e65b563 chore: bump version to 1.33.0 2026-04-15 09:28:37 +00:00
edde746 e0308be8bb fix: mounted check in _toggleLibraryVisibility 2026-04-15 11:04:16 +02:00
edde746 4663c435c9 fix: artwork directory null crash on orientation change 2026-04-15 06:11:07 +02:00
edde746 8380980abd fix: update background_downloader fork ref 2026-04-15 00:32:18 +02:00
edde746 771c431188 fix: treat 500 as permanent failure, show error in UI 2026-04-14 22:29:05 +02:00
edde746 1e1bb9176a feat: watch together buffering indicators 2026-04-14 22:07:13 +02:00
edde746 6eb5c847af fix: download cleanup, retry circuit breaker, SAF resume 2026-04-14 21:54:00 +02:00
edde746 e50fdc4eb9 feat: circular dpad and filled nav chips on remote screen 2026-04-14 13:29:42 +02:00
edde746 4edf1a8c42 fix: auto-start remote server after home data loads 2026-04-14 13:03:34 +02:00
edde746 cb455d379c feat: inline discovery in remote screen 2026-04-14 12:58:41 +02:00
edde746 d093023ce7 feat: replace QR remote pairing with encrypted LAN discovery
Replace session ID + PIN auth with HKDF-derived home secret and
per-session AES-256-GCM encrypted channel. Auto-start remote server
on host devices, auto-discover hosts via authenticated UDP broadcast.
2026-04-14 12:33:40 +02:00
edde746 a55650bc40 fix: skip QR scanner on devices without cameras 2026-04-14 06:53:51 +02:00
edde746 6cbf0980c5 fix: filter HTTPClientError and DiscordStateException from sentry 2026-04-14 06:43:53 +02:00
edde746 4cb65c1bfa feat: mouse back button navigation 2026-04-14 06:30:18 +02:00
edde746 f30e47dbd7 fix: enforce min 2x DPR for TV image transcoding
close #860
2026-04-14 05:55:43 +02:00
edde746 7ab6b9720f feat: increase subtitle sync slider range to ±60s
close #851
2026-04-14 04:24:53 +02:00
edde746 2f7c87d437 fix: subtitle background not rendering on mpv 0.38+
close #773
2026-04-13 23:59:47 +02:00
edde746 bc4b4955da feat: auto-remove watched downloads, server watched threshold
close #856
2026-04-13 22:46:49 +02:00
edde746 f7e3408c86 fix: guard empty list access in alpha jump bar and download tree 2026-04-13 22:07:21 +02:00
edde746 2126a38f39 fix: bounds-safe action bar focus node access 2026-04-13 22:00:36 +02:00
edde746 b81f6eaa96 feat: watch together recent rooms, plex usernames, action toasts, shorter codes
close #855
2026-04-13 14:02:21 +02:00
edde746 17eb37cb37 fix: back navigation on screens with no app bar actions
close #854
2026-04-13 11:33:15 +02:00
github-actions[bot] 9208aa8d16 chore: update cask to 1.32.0 2026-04-13 06:35:14 +00:00
github-actions[bot] bd7f7894ed chore: bump version to 1.32.0 2026-04-13 06:03:17 +00:00
edde746 6dfdb62df8 feat: relay connection limits and container memory caps 2026-04-13 07:55:28 +02:00
edde746 3be302d69f feat: set sentry environment per distribution channel 2026-04-13 02:38:28 +02:00
edde746 66ebc547f0 fix: filter disk-full and dbus socket errors from sentry 2026-04-13 02:05:24 +02:00
edde746 3366b83a89 fix: hide spoilers in watch next thumbnails
close #849
2026-04-12 11:12:37 +02:00
edde746 cd3db94b5d fix: guard library dropdown against empty list 2026-04-12 10:14:43 +02:00
edde746 d56067e7cc fix: skip back navigation when overlay route is on top 2026-04-12 10:11:11 +02:00
edde746 7c3cee692c fix: one-shot select key activation in focus helpers 2026-04-12 10:00:26 +02:00
edde746 a948d82c24 switch from glitchtip 2026-04-12 09:11:42 +02:00
edde746 77893bb9ba fix: detail screen section alignment 2026-04-12 01:37:08 +02:00
edde746 1b2ecd9de4 fix: standardize bool parsing with flexibleBool helper
close #844
2026-04-11 21:22:15 +02:00
edde746andGitHub b92842f9ac Merge pull request #842 from jordankoehn/fix-live-tv-join-session-edge-case
Fix Live TV join session edge case
2026-04-11 08:09:51 +02:00
edde746 91d191b3d8 fix: file info bitrate units, multi-track display, tv scrolling
close #840, close #841
2026-04-11 08:04:12 +02:00
Jordan Koehn 21accaa883 Fix Live TV join session edge case
Under the circumstance that guide data is not available, program start
is equal to current time. So program start should be ignored so one can
tune to the oldest available recording offset.
2026-04-10 22:34:32 -04:00
edde746 c6f2e949a7 fix: use configured skip setting for dpad/timeline seeking
close #751, close #790
2026-04-10 22:31:41 +02:00
edde746 9d9d9bb311 feat: add square art and logo editing to metadata editor
close #837
2026-04-10 18:38:54 +02:00
edde746 0b20300572 fix: noisy audio pause on android tv 2026-04-10 10:24:19 +02:00
edde746 f47bcb69fd feat: auto-hide performance overlay with controls
close #834
2026-04-10 01:30:26 +02:00
edde746 c20bb93f1f fix: always show track selection button
close #833
2026-04-09 23:52:02 +02:00
edde746 497259ca16 fix: close http clients on server replacement 2026-04-09 23:34:01 +02:00
edde746 4322ffc6e5 fix: disable android tv pip lifecycle regressions 2026-04-09 06:39:17 +02:00
edde746 8b40e0993f fix: normalize EOF playback state
close #828
2026-04-09 05:55:03 +02:00
edde746 966b6371f5 fix: preserve download server affinity 2026-04-09 04:51:35 +02:00
edde746 5b493a70b0 feat: show snackbar on playback speed change
close #824
2026-04-09 03:48:14 +02:00
edde746 1cb793d781 feat: add sub-ass-override setting 2026-04-08 19:35:18 +02:00
edde746 7f237c9c7b refactor: reuse showTextInputDialog for episode count input 2026-04-08 18:08:39 +02:00
edde746 d0934295b0 feat: download all playlist items
close #241
2026-04-08 12:37:03 +02:00
edde746 8d40ba3011 fix: reduce blank space in media card bottom 2026-04-08 07:21:21 +02:00
edde746 f926deb63b feat: clickable cast members to browse actor filmography
close #723
2026-04-08 05:17:37 +02:00
edde746 b82c612f94 fix: remove unused parameter in focusable_slider 2026-04-08 04:40:12 +02:00
edde746 e7ae108c49 chore: bump win_http to 0.2.0 2026-04-08 04:24:06 +02:00
edde746 08689df2f5 chore: update Podfile.lock 2026-04-08 01:58:26 +02:00
edde746 87c308f6ca feat: use win_http on Windows 2026-04-08 01:54:40 +02:00
edde746 15b22f75a8 refactor: replace dio with package:http 2026-04-08 00:43:08 +02:00
edde746 8e7431e7e5 feat: download options picker for series/seasons 2026-04-07 22:59:35 +02:00
edde746 c034f9b66e fix: request utf-8 encoding for external subtitles
close #817
2026-04-07 18:34:47 +02:00
edde746 5cf53f8cfa fix: chapter keyboard shortcuts navigate chapters not episodes
close #820
2026-04-07 17:58:00 +02:00
edde746 e4de7f1ecb fix(tv): reduce live stream reconnect delay for faster fallback 2026-04-07 15:37:36 +02:00
edde746 bf9913ba0f fix(tv): re-tune on live stream fallback, fix decision 400 throw 2026-04-07 10:52:54 +02:00
edde746 6ac8c905c9 fix(tv): live stream direct-stream fallback chain 2026-04-07 07:58:42 +02:00
edde746 603b5b5105 fix: pip aspect ratio clamp values outside android limits 2026-04-07 07:35:03 +02:00
edde746 7528dcec71 fix: connection selection failover and timeout budget 2026-04-07 06:49:51 +02:00
github-actions[bot] d96848181d chore: update cask to 1.31.3 2026-04-07 04:28:16 +00:00
github-actions[bot] 16b6ea8f29 chore: bump version to 1.31.3 2026-04-07 04:11:07 +00:00
edde746 7150ea2987 fix: log reason for https upgrade failure 2026-04-07 05:48:44 +02:00
edde746 bc2febf480 fix: prefer https endpoints, catch h2 protocol errors 2026-04-07 05:48:44 +02:00
github-actions[bot] 11ec48a506 chore: update cask to 1.31.2 2026-04-07 02:40:18 +00:00
github-actions[bot] f08989cc99 chore: bump version to 1.31.2 2026-04-07 02:21:14 +00:00
edde746 424351092d fix: h2 fallback to http/1.1 on handshake failure 2026-04-07 04:20:41 +02:00
edde746 df72c3e6f4 chore: bump build number 2026-04-07 02:11:40 +02:00
edde746 82e605a1c6 fix: don't close database on mobile suspend 2026-04-07 02:11:09 +02:00
github-actions[bot] 7a91675054 chore: update cask to 1.31.1 2026-04-06 22:43:34 +00:00
github-actions[bot] edf4eb2090 chore: bump version to 1.31.1 2026-04-06 22:20:12 +00:00
edde746 de1b37b7e9 fix: library density migration crash on upgrade
close #812
2026-04-07 00:19:27 +02:00
edde746 928206a179 fix: log image load failures with rate limiting 2026-04-06 22:53:13 +02:00
edde746 52b8944d71 feat: use HTTP/2 for all network requests 2026-04-06 22:53:13 +02:00
github-actions[bot] 0db5cb7025 chore: update cask to 1.31.0 2026-04-06 20:51:22 +00:00
edde746 cef8f5efe2 fix: use sentry-dart fork to fix Windows build 2026-04-06 22:31:37 +02:00
github-actions[bot] 83ff87c79f chore: bump version to 1.31.0 2026-04-06 14:50:41 +00:00
edde746 00f5357403 rename "Episode Thumbnail" to "Thumbnail" in all translations 2026-04-06 16:45:12 +02:00
edde746 010ace58fa fix: dispose TextEditingControllers in settings dialogs 2026-04-06 16:43:04 +02:00
edde746 b3cf4ebf12 fix: await hidden libraries init before loading content 2026-04-06 16:19:23 +02:00
edde746 f707b7b5f2 Merge branch 'fix-live-tv-never-prompt-to-watch-from-start-of-recording' 2026-04-06 04:12:44 +02:00
edde746 29ab2ddad3 refactor: fix variable naming, indentation, remove debug log 2026-04-06 04:12:33 +02:00
edde746 627024bd13 fix: reduce spacing 2026-04-06 03:02:36 +02:00
edde746andGitHub c4a71048f5 Merge pull request #808 from eConnah/update-readme-nix
docs: update Nix package link to official search and add new listed co-maintainer
2026-04-06 02:45:18 +02:00
Connor Alecks bd07b7f011 docs: update Nix package link to official search and add new listed co-maintainer 2026-04-06 00:54:37 +02:00
Jordan Koehn 93496849ec fix: Live TV uses wrong begins at time 2026-04-05 11:54:27 -04:00
edde746 9984f42176 fix: tabular figures on timeline timestamps 2026-04-04 23:46:43 +02:00
edde746 a262c94f88 refactor: extract FocusableSlider widget 2026-04-04 19:22:23 +02:00
edde746 8f326e692d fix: dpad nav trapping in density slider 2026-04-04 16:30:13 +02:00
edde746 7b8a47aac1 fix: include previous episodes in play queue 2026-04-04 16:05:44 +02:00
edde746 b46f1cccb1 remove chapter_marker_painter 2026-04-04 01:36:03 +02:00
edde746 5880451401 fix: try DV conversion before MPV fallback on decoder hang 2026-04-04 00:19:10 +02:00
edde746 3c59452782 fix: subtitle track matching (forced flag parsing, title scoring) 2026-04-03 23:53:37 +02:00
edde746 7d35132509 feat: M3 2024 slider style for video player timelines
Use GappedTrackShape (no stop indicator) globally.
Timeline/volume sliders get local overrides for thinner
track, zero padding, and no overlay. Chapter markers
rendered as track gaps. Live TV timeline matches style.
Timeline thumb hidden when unfocused in dpad mode.
2026-04-03 23:14:11 +02:00
edde746 dbb3e14c50 fix: skip offline video when different version requested 2026-04-03 17:50:36 +02:00
edde746 d1d45c4998 refactor: restyle subtitle styling and external player settings screens 2026-04-03 17:25:02 +02:00
edde746 c9db6e528e feat: more consistent poster sizing
close #798
2026-04-03 17:04:20 +02:00
edde746 062fd82de1 fix: recover audio after Windows sleep via ao-null-reload 2026-04-03 12:19:36 +02:00
edde746 122cff359d fix: await database close on desktop app exit 2026-04-03 12:19:29 +02:00
edde746 ef96ccb623 fix: ordinal tiebreaker for identical track matching 2026-04-03 00:46:01 +02:00
edde746 8a66cf6d92 fix: add HID key labels for non-keyboard USB pages
close #793
2026-04-02 22:04:26 +02:00
edde746 16dd352231 fix: hide live badge when timeshifted
close #796
2026-04-02 17:35:52 +02:00
edde746 fb275ea242 refactor: replace iso 639 json asset with const dart map 2026-04-02 17:07:45 +02:00
edde746 533472299a feat: add search filter to collection selection dialog
close #784
2026-04-02 16:38:37 +02:00
edde746 da95901d39 fix: Windows native code bugs and dedup
- fix unsigned underflow in Utf8FromUtf16 when WideCharToMultiByte fails
- fix inverted freopen_s logic in console attach
- remove misleading WS_EX_APPWINDOW from GWL_STYLE mask
- close mutex handle on normal exit
- extract SetHDRStateForTarget, EnableComposition/DisableComposition, DisplayModeToMap helpers
- cache GetWindowsVersion result
2026-04-02 15:02:07 +02:00
edde746 29101c310b fix: clean up power-resume timer on dispose and destroy 2026-04-02 14:42:14 +02:00
edde746 91db6d0e57 fix: reload audio output on Windows wake from sleep 2026-04-02 14:36:50 +02:00
edde746 504da89223 fix: focus search input on tab selection
close #795
2026-04-02 14:18:02 +02:00
edde746 4a54b897ac fix: guard sdk_26100.h for newer Windows SDKs 2026-04-02 13:22:27 +02:00
edde746 9565682eda feat: Windows display mode matching (refresh rate, HDR) 2026-04-02 13:05:58 +02:00
edde746 17741a2f61 fix: live timeline focus style 2026-04-02 02:40:13 +02:00
edde746 f5407ecac8 fix: enable PiP and crop/fill for live TV 2026-04-02 02:38:16 +02:00
edde746 c57cbfb2f6 fix: live TV time-shift controls and timeline 2026-04-02 02:29:04 +02:00
edde746 339a767304 refactor: M3 settings screen redesign 2026-04-02 00:33:25 +02:00
edde746 4c534b44d4 fix: consistent borderless text input styling 2026-04-01 23:10:40 +02:00
edde746 984125a79e fix: subtitle search UI polish and keyboard input 2026-04-01 22:36:51 +02:00
edde746 17f1075e64 feat: subtitle search & download from OpenSubtitles
close #343
2026-04-01 22:20:59 +02:00
edde746 64a5874566 fix: close database on app shutdown 2026-04-01 20:30:05 +02:00
edde746 b16a6a6bd5 fix: companion remote acts as remote instead of host on Android TV 2026-04-01 16:23:31 +02:00
edde746 87da5b400e chore: remove tune debug logging 2026-04-01 16:05:36 +02:00
edde746 3f1da5f289 feat: live TV time-shift 2026-04-01 12:20:07 +02:00
edde746 45525a4f14 feat: live TV favorite channel ordering
close #780
2026-04-01 06:32:27 +02:00
edde746 fc7892daed fix: option picker dialog focus for TV/keyboard nav 2026-04-01 06:30:08 +02:00
edde746 51a49cfca3 fix: disable Impeller on Huawei/HONOR devices 2026-03-31 14:16:01 +02:00
edde746 da8b583286 feat: tag editing in metadata editor 2026-03-31 14:10:17 +02:00
edde746 2c12a07257 fix: reuse AudioTrack across seeks, reduce PCM buffer 2026-03-31 12:39:10 +02:00
edde746 3a82119c2a fix: library action confirmation and snackbar feedback 2026-03-31 10:37:10 +02:00
edde746 e6ea1b056a fix: overlay sheet builder context popping main route
close #779
2026-03-31 10:30:25 +02:00
edde746 1a96d2b418 feat: download version picker with smart series matching
close #767
2026-03-31 04:46:09 +02:00
edde746 8eb716505d fix: mpv dispose ANR on main thread 2026-03-30 21:42:11 +02:00
edde746 ddb157809a feat: play version context menu option 2026-03-30 08:03:39 +02:00
edde746 ad8faab9ea fix: android video surface lifecycle management
close #771
2026-03-29 18:07:50 +02:00
edde746 6a7644935f chore: remove steam scripts 2026-03-28 20:16:48 +01:00
edde746 684196aa36 perf: type-aware image memory cache bounds 2026-03-28 18:31:16 +01:00
edde746 56697303f9 fix: async mpv dispose on macOS 2026-03-28 17:55:34 +01:00
edde746 3306a28892 fix: isolate libdovi from JVM heap, reuse DV buffers 2026-03-28 17:38:11 +01:00
edde746 059509354d feat: configurable watch together relay server 2026-03-28 14:44:30 +01:00
edde746 53d50dbc3b fix: guard scroll operations against infinite extents 2026-03-28 13:51:39 +01:00
edde746 46ebda944a fix: flexible int parsing in PlexRole 2026-03-28 13:44:57 +01:00
edde746 0bb200a785 feat: play from beginning context menu option
close #761
2026-03-27 17:43:15 +01:00
edde746 d3602201c6 refactor: remove dead code 2026-03-27 17:39:43 +01:00
edde746 beb78fada0 fix: eager artwork dir init, guard back nav on root 2026-03-27 11:06:22 +01:00
edde746 a05efef3c8 fix: guard download action button focus node access 2026-03-27 10:39:50 +01:00
edde746 36d63860e2 chore: update pubspec.lock 2026-03-27 09:39:50 +01:00
edde746 ad9be7f132 Merge branch 'feature/server-tasks' 2026-03-27 09:38:24 +01:00
edde746 8e9968628a fix: server tasks panel polish 2026-03-27 09:37:53 +01:00
edde746 e33db4f6d7 fix: auto-focus skip intro button on TV when controls visible
close #742
2026-03-26 20:33:23 +01:00
edde746 ab5ad3c839 fix: use hubs API for continue watching
close #748
2026-03-26 18:58:18 +01:00
github-actions[bot] fa4af97fb0 chore: update cask to 1.30.0 2026-03-26 10:03:52 +00:00
github-actions[bot] db50cb4837 chore: bump version to 1.30.0 2026-03-26 09:05:04 +00:00
edde746 b4b90eec63 fix: use forked material_symbols_icons for IconData compat 2026-03-26 08:32:16 +01:00
edde746 beff313814 fix: use Flutter 3.41.5 for arm64 builds 2026-03-26 07:12:09 +01:00
edde746 0dc9cd38af fix: pin arm64 Flutter version before IconData final 2026-03-26 00:45:18 +01:00
edde746 8ff7676e60 fix: use continuous play queue for aired date episode order
close #756
2026-03-25 23:28:57 +01:00
edde746 bfe35220aa fix: skip SAF picker on Android TV 2026-03-25 20:56:32 +01:00
edde746 843faee4c5 fix: run sqlite on background isolate 2026-03-25 20:50:35 +01:00
edde746 f8bfeb0ec6 fix: use stable Flutter channel for arm64 builds 2026-03-25 20:34:08 +01:00
edde746 dd441fee66 fix: ignore EOF completion events for live TV streams 2026-03-25 19:51:11 +01:00
edde746 2204b05d6c fix: remove reconnect_at_eof from live TV stream options 2026-03-25 19:10:35 +01:00
edde746 a1841804b6 fix: non-primary CustomScrollViews in IndexedStack tabs 2026-03-25 19:10:35 +01:00
Micah Morrison 25e3c4a543 Show server tasks 2026-03-25 11:33:01 -04:00
edde746andGitHub 5e31ad9c67 Merge pull request #753 from micahmo/fix/single-season
Fix missing season tabs
2026-03-25 15:45:11 +01:00
Micah Morrison 477b92291b Fix missing season tabs 2026-03-25 09:20:49 -04:00
edde746 fcc6186796 fix: dedupe live TV timeline heartbeat, delay initial send 2026-03-25 12:25:19 +01:00
edde746 23179f6d17 perf: ValueNotifier detail scroll, skip redundant arrow rebuilds 2026-03-25 08:12:18 +01:00
edde746 996e8621fa perf: ValueNotifier scroll state, static skeletons, scoped selects 2026-03-25 08:03:47 +01:00
edde746 8ad1cb988f fix: detach mpv surface before view removal in dispose 2026-03-25 07:25:32 +01:00
edde746 4cef99dda2 perf: coalesce watch together rebuilds, static TV overlays 2026-03-25 06:40:17 +01:00
edde746 4beeedd318 fix: library grid stuck skeletons on fast scroll 2026-03-25 06:35:01 +01:00
edde746 23043c61f4 fix: Tensor SoC detection for internal model IDs 2026-03-25 04:15:34 +01:00
edde746 8d56171bd2 perf: library grid scroll and idle frame rendering
- Window _loadedItems and focus nodes to bound memory growth
- Eager prefetch and image precaching near viewport
- Static skeleton placeholders during active scroll
- Throttle alpha bar updates, dynamic initial fetch size
- Fix discover timer scheduling frames when tab hidden
- Disable tickers for offscreen IndexedStack children
- Static image placeholder with fade-in transition
2026-03-24 17:19:28 +01:00
edde746 ff0fafe938 fix: speed step 0.1 -> 0.25 for keyboard shortcuts
close #741
2026-03-23 15:02:13 +01:00
edde746 8b7f369c16 fix: keyboard shortcuts for unmapped keys
close #740
2026-03-23 14:32:54 +01:00
github-actions[bot] 5c0e40bddb chore: update cask to 1.29.0 2026-03-23 05:22:26 +00:00
github-actions[bot] 58b0bf5306 chore: bump version to 1.29.0 2026-03-23 02:51:31 +00:00
edde746 9bd2774820 fix: race conditions in mpv dispose and track manager, add debug fallback trigger 2026-03-23 03:48:48 +01:00
edde746 cc7da5bbc9 fix: live tv channels for non-admin users 2026-03-22 15:58:25 +01:00
edde746 bd4e30e653 fix: external subtitles on android mpv 2026-03-22 15:34:49 +01:00
edde746 e759dafa87 fix: disable impeller on Tensor/NVIDIA GPUs 2026-03-22 14:35:42 +01:00
edde746 3ae2488adf feat: screen lock
close #286
2026-03-22 11:48:34 +01:00
edde746 2f5b596fff fix: disable pip on android tv
close #698
2026-03-22 11:17:45 +01:00
edde746 472ac90fea feat: rewind on resume setting
close #735
2026-03-22 09:24:22 +01:00
edde746 e9dfee6870 fix: profile select back button bypass
close #737
2026-03-22 09:03:14 +01:00
edde746 48e57fd375 fix: match content frame rate
close #736
2026-03-22 08:12:16 +01:00
edde746 4b05ed79ea fix: bump libmpv-android to v1.0.6 2026-03-22 04:59:31 +01:00
edde746 4365e70a78 fix(android): libmpv failed init reinit 2026-03-21 16:51:25 +01:00
edde746 e8c02d04ae fix: linux titlebar height on non-GNOME DEs 2026-03-21 15:57:03 +01:00
edde746 0539c1d869 fix: overlay sheet position, scroll-to-selected in bottom sheets 2026-03-21 11:49:08 +01:00
edde746 1980ce92d5 fix: pip aspect ratio after exoplayer→mpv fallback 2026-03-21 09:19:21 +01:00
edde746 b5576fa6de fix: reduce hub section vertical spacing 2026-03-21 09:05:21 +01:00
edde746 d355e07e10 refactor: switch to libmpv-android fork, fix ANR and track selection 2026-03-21 08:53:08 +01:00
edde746 09ff47e19f fix: reduce CPU when paused and backgrounded on macOS 2026-03-21 08:36:25 +01:00
edde746 4ee4a766d6 fix: pip background thread layout crash 2026-03-21 07:54:04 +01:00
edde746 7ebbf75cb9 fix: handle background isolate shutdown on iOS 2026-03-21 01:43:34 +01:00
edde746 e95b768350 chore: update appcast.xml url 2026-03-21 00:04:10 +01:00
edde746 82f0d46acc fix: update libass 2026-03-20 09:07:17 +01:00
edde746 bafba0b151 fix: guard cache put against non-Map response data 2026-03-20 08:29:36 +01:00
edde746 7a68218439 fix: guard setState with mounted in pairing screen 2026-03-20 08:07:29 +01:00
edde746 2d37d37596 refactor: extract TrackManager, fix subtitle bugs
- Extract track lifecycle logic from VideoPlayerScreen (3248→2928 lines) into TrackManager
- Parse isDefault/isForced for subtitle tracks in parseTrackList
- Emit forced flag from ExoPlayer's emitTrackList
- Fix detectSubtitleMimeType failing on URLs with query params
- Add missing external_ ID prefix in ExoPlayer addSubtitleTrack
- Replace polling loop with stream in selectAndApplyTracks
- Handle play() failure in resumeAfterSubtitleLoad
- Handle addExternalSubtitles failure in onBackendSwitched
2026-03-20 03:32:32 +01:00
edde746 d64c72f40e fix: await external subs before track selection in backend fallback 2026-03-20 00:05:18 +01:00
edde746 efe375f817 fix: check track state before subscribing to broadcast stream 2026-03-19 23:38:50 +01:00
edde746 1f8dfd3587 fix: regenerate macos Podfile.lock 2026-03-19 21:48:01 +01:00
edde746 60eae5d14f fix: reapply track selection and external subs after MPV fallback 2026-03-19 21:37:37 +01:00
edde746 5123a5ec48 chore: bump dependencies 2026-03-19 20:53:35 +01:00
edde746 dbb3ae38b3 chore: bump sentry_flutter to 9.15.0 2026-03-19 19:56:36 +01:00
edde746 fbf6fc988b chore: update libass-android 2026-03-19 19:52:05 +01:00
edde746 17da465e4e chore: bump mobile_scanner to 7.2.0 2026-03-19 19:49:57 +01:00
edde746 e096a4ccb8 fix: use direct TrackGroup refs for track selection, clear stale overrides 2026-03-19 18:58:07 +01:00
github-actions[bot] 079417ad3e chore: update cask to 1.28.0 2026-03-19 05:06:01 +00:00
github-actions[bot] edb2120f98 chore: bump version to 1.28.0 2026-03-19 04:40:54 +00:00
edde746 cc0305d84a fix: set leafCount for offline season metadata 2026-03-19 05:14:17 +01:00
edde746 3f78686661 fix: download deletion using DB file paths
close #721
2026-03-19 05:07:27 +01:00
edde746 13604cca01 fix: defer nativeSurfaceChanged out of layout pass 2026-03-18 21:13:57 +01:00
edde746andGitHub eae4eacf00 Merge pull request #719 from micahmo/fix/delete-propagation-flat
Fix delete propagation when episodes are displayed in flat list
2026-03-18 17:00:55 +01:00
Micah Morrison 4119824048 Fix delete propagation when episodes are displayed in flat list 2026-03-18 11:58:46 -04:00
edde746 8bdf2b53d3 fix: reduce bottom sheet spacing 2026-03-18 09:46:48 +01:00
edde746 fb4f17a898 feat: support MKV ContentCompAlgo 0 (zlib) in ExoPlayer 2026-03-18 08:52:14 +01:00
edde746 ad0623334c feat: approximate seeking for MKV files without Cues 2026-03-18 08:52:14 +01:00
edde746 13dcfd7551 fix: load downloaded subtitles for offline playback
close #718
2026-03-18 08:52:14 +01:00
edde746 053884a147 fix: use gpu-api=auto for mpv
close #715
2026-03-18 02:36:29 +01:00
edde746 4f893cb826 fix: incorrect queue sheet item sizing 2026-03-17 10:45:04 +01:00
edde746 94aa966f46 fix: enable WAL mode for sqlite 2026-03-17 10:31:39 +01:00
edde746 0fcdcf3c1f fix: reduce image cache budgets and add memory pressure handling 2026-03-17 09:48:18 +01:00
edde746 4147bd0d86 feat: overlay timestamp inside timeline thumbnail preview 2026-03-16 08:03:28 +01:00
edde746 3ea3455772 fix: prevent SurfaceControl use-after-free
Synchronously null surface fields in dispose() before deferred view
removal, add session generation counter to cancel stale MPV fallbacks,
and use postAtFrontOfQueue for defense-in-depth message ordering.
2026-03-16 06:15:12 +01:00
edde746 a82c3d2d2d feat: add grouping and type filtering for shared libraries
Show grouping chip for shared (mixed-type) libraries with options:
All, Movies, TV Shows, Seasons, Episodes. Filter with type=1,2,3,4
to exclude library section directory entries from /library/shared/all.
2026-03-16 05:58:36 +01:00
edde746 04ef1b8ad2 fix: guard against missing library in libraries screen
Use firstOrNull instead of firstWhere to prevent StateError crash
when selected library is temporarily absent from provider during
server re-probe after LOW_MEMORY events.
2026-03-16 02:45:01 +01:00
edde746 b009dd9c80 chore: remove unused workmanager and android_intent_plus dependencies 2026-03-15 22:55:18 +01:00
edde746 e9f1979e6c refactor: remove dead getters from model classes 2026-03-15 22:55:18 +01:00
edde746 23906dd65d refactor: remove dead code from provider files 2026-03-15 22:55:18 +01:00
edde746 630755cf7b refactor: remove dead utility methods
Remove filterOutMusic, watchUserProfile, and watchHiddenLibraries
2026-03-15 22:55:18 +01:00
edde746 3c2f72d15f refactor: remove dead methods from services 2026-03-15 22:55:18 +01:00
edde746 ff5cf06e60 chore: remove HttpTimingTransferListener debug logging 2026-03-15 22:47:51 +01:00
edde746 9798486724 chore: bump cronet 2026-03-15 22:38:06 +01:00
edde746 db6161d84e refactor: replace numeric tab indexes with NavigationTabId 2026-03-15 22:31:54 +01:00
edde746 af0de5c0a9 fix: switch from OkHttp to Cronet 2026-03-15 22:00:51 +01:00
edde746 e0d9b4ee93 refactor: unify focus restoration for main screen tabs
Replace per-tab if-chains in _focusContent() and _selectTab() with
generic FocusableTab/TabVisibilityAware dispatch. Fixes search screen
focus on Android TV (sidebar → Search now works).
2026-03-15 21:55:22 +01:00
edde746 551be5bbef feat: dynamic library tabs and shared library navigation
- Replace hardcoded 4-tab system with data-driven visible tab list
- Shared libraries show only Browse + Playlists tabs
- Fix dpad navigation for shared libraries (no hidden tab targets)
- Switch to TickerProviderStateMixin to support tab controller recreation
- Navigate to library screen when tapping shared library sections
- Persist tab selection by name instead of index
2026-03-15 21:36:17 +01:00
edde746 bbc7f0f665 fix: focus search input on search tab 2026-03-15 19:21:34 +01:00
edde746 8433d59432 feat: add Cmd/Ctrl+F shortcut to open search
close #705
2026-03-15 19:02:50 +01:00
edde746 59b3e6ce81 fix: eliminate duplicate API requests on startup
- Skip redundant notifyListeners in HiddenLibrariesProvider init when
  set is empty (empty→empty is a no-op for consumers)
- Track last-seen hidden keys in DiscoverScreen to avoid reloading
  content when the listener fires without an actual change
- Track previous online server set in MultiServerProvider; only re-check
  DVR availability when a new server comes online
- Remove dead content-loading pipeline from libraries_screen (sorts,
  pagination, filters, items) that was never rendered
- Move /media/providers fetch into PlexClient.create() init, replacing
  lazy EPG cache and providing libraries including shared items
2026-03-15 18:23:32 +01:00
edde746 a5629c54f6 feat: mask unfocused digits in TV PIN input 2026-03-15 14:18:25 +01:00
edde746 45726a57ce feat: log git commit hash 2026-03-15 13:51:09 +01:00
edde746 5f597ef567 fix: prefer exact language code match in track selection
Fixes #703
2026-03-15 11:40:45 +01:00
edde746 bf464b257d fix: log full exception chain on ExoPlayer errors 2026-03-15 11:38:07 +01:00
edde746 0100b2a2ea fix: preserve start position and subtitles in ExoPlayer→MPV fallback 2026-03-15 11:32:25 +01:00
edde746 360c3356cf chore: remove 27 unused translation strings 2026-03-15 07:57:45 +01:00
edde746 5f18efcfb3 feat: live TV channel favorites
close #663
2026-03-15 07:44:44 +01:00
edde746 79888168ae fix: add timeout diagnostics 2026-03-15 05:06:16 +01:00
edde746 2427a55901 fix: dispatch EventChannel calls to main thread 2026-03-15 03:11:23 +01:00
edde746 ac397f32a3 feat: redesign TV player controls
- BACK key dismisses layers (content strip → controls → exit) with
  early BackKeyCoordinator marking to prevent PopScope race condition
- Hide volume control and PiP button on TV
- Fix focus chain with mounted-node-aware navigation for live/non-live
- Fix focus reclaim on controls hide (hasPrimaryFocus vs hasFocus)
  to prevent screen-level self-heal from stealing timeline focus
- LEFT/RIGHT shows controls with timeline focused for seeking

Close #701
2026-03-15 03:10:51 +01:00
edde746 d236e7fec6 fix: tighten and align media detail screen spacing 2026-03-15 01:31:18 +01:00
edde746 6d7cdc4ef4 fix: preserve chapters/markers in cache during watch state sync 2026-03-15 01:04:13 +01:00
edde746 9a296b3da8 fix: guard scrollListToIndex against multiple scroll positions 2026-03-14 22:11:48 +01:00
edde746 f27861748c ci: push appcast.xml to dedicated branch on release 2026-03-14 19:00:11 +01:00
edde746 c9668af2ae feat: sort home screen hubs by user's library order
Closes #696
2026-03-14 19:00:11 +01:00
github-actions[bot] 9c2253616f chore: update cask to 1.27.2 2026-03-14 15:05:21 +00:00
edde746 cfb8bf04d6 fix: reduce remote playback startup latency
- Lower OkHttp readTimeout from 30s to 10s so stalled Range requests retry faster
- Reduce bufferForPlaybackMs from 2500 to 1000 for quicker playback start
- Add HTTP transfer timing logs (TTFB, bytes, duration) to diagnose startup delays
2026-03-14 12:15:53 +01:00
github-actions[bot] 9efdd5a17d chore: bump version to 1.27.2 2026-03-14 10:21:54 +00:00
edde746 dd11e8d3b2 fix: scroll list tiles into view on dpad focus 2026-03-14 11:20:31 +01:00
edde746 cde6e99a17 fix: dpad nav skipping episodes on single-season shows
close #691
2026-03-14 11:09:01 +01:00
edde746 045e68ca10 chore: update dart_discord_presence to 1.1.1 2026-03-14 05:41:02 +01:00
edde746 4f736c555b fix: make PlexMetadata type/key/title nullable for folder items
Plex API returns Directory entries without type field, causing
TypeError crash in fromJson. Made fields nullable and migrated
callers to use mediaType enum and displayTitle getter instead
of raw string access.
2026-03-14 05:40:14 +01:00
github-actions[bot] 642c49b1ea chore: bump version to 1.27.1 2026-03-14 04:03:34 +00:00
edde746 e13b16f13c fix(android): mpv boolean NPE on dispose 2026-03-14 05:01:39 +01:00
github-actions[bot] 1f9fa9ba4f chore: update cask to 1.27.0 2026-03-14 01:17:01 +00:00
github-actions[bot] 849ef6efe5 chore: bump version to 1.27.0 2026-03-14 00:53:01 +00:00
edde746 e968a20f4c fix(livetv): soften EPG grid program styling 2026-03-13 18:49:40 +01:00
edde746 7a6b866c28 fix(livetv): fetch boundary-spanning programs
close #686
2026-03-13 18:45:00 +01:00
edde746 6ccbaed3fd fix(livetv): always bold EPG program titles in guide grid
close #687
2026-03-13 18:20:47 +01:00
edde746 d01628a262 fix(android): use OkHttp connection pooling to prevent high-bitrate stutter
Replace DefaultHttpDataSource with OkHttpDataSource for TCP connection
reuse on buffer refill. Increase buffer sizes for devices with >=1GB RAM.
2026-03-13 17:23:06 +01:00
edde746 675ac84486 fix: use settings-style bottom sheet for browse grouping picker 2026-03-13 12:05:51 +01:00
edde746 e84dc93910 fix: prevent tab revert and overflow bleed on mobile tab switch
Skip auto-focus in touch mode to prevent ensureVisible() from fighting
TabBarView animations, and clip each tab with ClipRect to stop hub row
overflow bleeding into adjacent tabs during swipe transitions.
2026-03-13 00:09:24 +01:00
edde746 3d30ea1342 fix(macos): reduce idle CPU when video player is paused
- Remove vulkan-swap-mode mailbox (continuous GPU rendering even when paused)
- End playback activity assertion on pause, restore on play
- Fix PerformanceStatsService persistent frame callback leak
2026-03-12 23:22:58 +01:00
edde746 efd28d3ca3 docs: add AUR package to readme 2026-03-12 13:20:16 +01:00
edde746 8a28a2c398 fix: only autofocus bottom sheets in keyboard/dpad mode 2026-03-12 05:02:52 +01:00
edde746 c6fa6afa9b fix: android thread safety
- ExoPlayerPlugin: move all MPV JNI calls off main thread via runOnExecutor
- WatchNextPlugin: run ContentProvider ops on IO executor with error handling
- MpvPlayerCore: guard executor methods against dispose race (RejectedExecutionException)
- MainActivity: fix PiP null engine NPE with safe flutterEngine?.let
2026-03-12 03:34:32 +01:00
edde746 912ebc2f90 fix: cooldown resume health probes, debounce connectivity, coalesce concurrent checks
Add per-platform cooldown on resume health probes (10s mobile, 2min
desktop). Debounce connectivity listener by 2s. Coalesce concurrent
checkServerHealth and reconnectOfflineServers into single in-flight
futures.
2026-03-12 02:52:14 +01:00
edde746 f04b49e260 fix: guard mpv event handler during disposal 2026-03-12 02:44:50 +01:00
edde746 a17458ff9b fix: handle DBusServiceUnknownException on Linux without NetworkManager 2026-03-12 02:24:00 +01:00
edde746 c062dd944e feat: inline season tabs and d-pad focus improvements 2026-03-12 00:57:51 +01:00
edde746 1a4516bdd1 Merge branch 'feature/respect-season-mode' 2026-03-11 23:09:23 +01:00
edde746 5538e0627b refactor: remove SeasonDetailScreen, unify into MediaDetailScreen
Extract EpisodeCard to its own widget file. MediaDetailScreen now handles
isSeason by showing episodes directly, with proper watch state tracking,
deletion handling, and D-pad focus navigation. Hide empty overview sections.
2026-03-11 23:08:39 +01:00
edde746 7febfa9771 fix: deduplicate episode fetching and use lazy list for flattened seasons 2026-03-11 22:35:30 +01:00
Micah Morrison 160be52f02 Fix parsing setting value 2026-03-11 17:13:11 -04:00
edde746 828d57af24 fix: auto-reconnect stream after network loss during playback 2026-03-11 22:08:11 +01:00
edde746 1f7fb2201c fix: handle null ratingKey in folder browsing 2026-03-11 22:08:11 +01:00
Micah Morrison 32ae51142f Respect the Plex server library season view mode 2026-03-11 16:42:49 -04:00
edde746andGitHub 7014df7a0e Merge pull request #680 from micahmo/fix/always-on-top
Fix issue with always on top getting stuck
2026-03-11 20:50:49 +01:00
Micah Morrison cb0ed15c0c Fix issue with always on top getting stuck 2026-03-11 15:00:17 -04:00
edde746 d4e9364d29 fix: handle TogglePlayPauseEvent for macOS media key
close #679
2026-03-11 18:28:07 +01:00
edde746 4681d60850 fix: hide admin actions from non-admin users
close #677
2026-03-11 18:18:27 +01:00
edde746 78052dc07f fix: player stuck on skip credits with no next episode
close #678
2026-03-11 18:02:33 +01:00
edde746 67c66ff0c1 fix: prevent layout shift for image placeholders 2026-03-11 15:02:38 +01:00
edde746 d1bb99635a feat: configurable intro/credits marker regex patterns
Add settings to customize regex patterns for matching intro and credits
markers in chapter titles, with the current hardcoded values as defaults.
Also reclassifies real markers with non-standard type strings against the
configured patterns.
2026-03-11 15:00:24 +01:00
github-actions[bot] 69092e3656 chore: update cask to 1.26.1 2026-03-11 13:32:42 +00:00
github-actions[bot] 98138a4f35 chore: bump version to 1.26.1 2026-03-11 13:09:47 +00:00
edde746 25c7f325b6 feat: use git commit SHA for sentry release version 2026-03-11 14:09:14 +01:00
edde746 5dbaf961b7 fix: ASS subtitles broken when DV mode enabled
close #674
2026-03-11 14:04:21 +01:00
edde746 b4471fab49 fix: add D3D11 fallback for gpu-api on Windows 2026-03-11 13:54:21 +01:00
edde746 307fd354d6 fix: make overlay sheets dismissible when no descendant is focused
close #673
2026-03-11 13:30:07 +01:00
edde746 a3b5557185 fix: use stream-lavf-o instead of stream-lavf-o-append for reconnect
stream-lavf-o-append is an mpv "action" that only works as an
init option, not a runtime property — all calls returned -3.
Using stream-lavf-o as a single property set works correctly.
2026-03-11 09:40:39 +01:00
edde746 3c491be690 fix: handle mpv end-file errors with global snackbar 2026-03-11 09:29:16 +01:00
edde746 580c1d82e5 fix: stuck network check on Linux without NetworkManager
close #670
2026-03-11 08:55:25 +01:00
github-actions[bot] 4cc83006ec chore: update cask to 1.26.0 2026-03-11 04:33:26 +00:00
edde746 4954c9c569 fix: replace tunneling reopen with simple parameter toggle
Removes the full player restart cycle for tunneling changes that was
causing infinite spinning/blank screen on Amlogic devices (Onn 4K Pro).
2026-03-11 05:08:43 +01:00
edde746 8f1b40d143 fix: handle string childCount in collections 2026-03-11 05:08:43 +01:00
github-actions[bot] d72153c25a chore: bump version to 1.26.0 2026-03-11 04:08:09 +00:00
edde746 666b59c293 refactor(native-player): share player helpers 2026-03-11 01:30:11 +01:00
edde746 7543a8c55b refactor: clean up windows code 2026-03-10 22:24:14 +01:00
edde746 bfc7be2b80 fix: default to Vulkan on Windows
close #653
2026-03-10 19:42:43 +01:00
edde746 8b2c68f3ae chore: switch Windows libmpv builds to SourceForge 2026-03-10 19:08:04 +01:00
edde746 9aa019fe6f fix: ANR in detachSurface 2026-03-10 19:07:33 +01:00
edde746 23a100afdc feat: auto-reconnect VOD playback after network loss 2026-03-10 18:47:31 +01:00
edde746 af2ac2c993 fix: suppress ASS GL thread crash, update ass-media to 0.4.0 2026-03-10 06:41:43 +01:00
edde746 166e73cdaa fix: sync button focus nodes with actual action count 2026-03-10 06:08:55 +01:00
edde746 73fe9e054f fix: guard unsafe .first/.last calls to prevent StateError 2026-03-10 03:50:12 +01:00
edde746 42e066288f fix: clickable title event bubbling and movie support 2026-03-10 01:57:28 +01:00
edde746 682a04f047 feat: show per-server connection status on splash screen 2026-03-10 01:50:19 +01:00
edde746 29cb88e6e2 fix: translate remaining strings 2026-03-10 01:21:03 +01:00
edde746 6daf77c890 feat: convert settings dialogs to platform-adaptive menus
Replace AlertDialog-based selection dialogs with popup dropdowns on
desktop/TV and bottom sheets on mobile, matching the context menu
pattern. Fix popup background blending into cards by tinting surface.
2026-03-10 01:18:33 +01:00
edde746 0372840ee1 fix: localize EPG day names and reduce channel column width
close #661
2026-03-10 00:19:16 +01:00
edde746 3c51760203 feat: mouse-only clickable text on media cards 2026-03-09 23:56:24 +01:00
edde746 f3c735e3a1 fix: remove Docker bridge IP filter 2026-03-09 17:35:28 +01:00
edde746 7784cb7511 fix: TMDB icon gradient 2026-03-09 16:24:59 +01:00
edde746 82b0de9223 feat: display movie edition titles across all views 2026-03-09 16:16:37 +01:00
edde746 a6852d716f fix: handle null fields in PlexHome deserialization 2026-03-09 15:42:33 +01:00
edde746 92dfe5b41f fix: lazy Sparkle init to prevent macOS startup hang 2026-03-09 14:39:16 +01:00
edde746 16e96cdde0 fix: handle null fields in folder metadata deserialization 2026-03-09 14:10:37 +01:00
edde746 61e4bda616 fix: content strip text colors in light mode
close #659
2026-03-09 12:53:16 +01:00
edde746 e69fab87d0 fix: handle DVR tune error responses and show to user 2026-03-09 11:33:54 +01:00
edde746 a3e738773b fix: handle Metadata as list in DVR tune response 2026-03-09 10:52:57 +01:00
github-actions[bot] 425957c804 chore: update cask to 1.25.1 2026-03-09 06:39:08 +00:00
github-actions[bot] efa1cc8bd8 chore: bump version to 1.25.1 2026-03-09 06:22:29 +00:00
edde746 0fc64cdf81 fix: catch seek PlatformException at source, remove coordinator
Catch COMMAND_FAILED/NOT_INITIALIZED in PlayerNative.seek() and
PlayerAndroid.seek() directly, preventing unhandled PlatformException
crashes on macOS app resume. Remove the ~200-line
_PendingSeekCoordinator system and revert all callsites to direct
player.seek(clampSeekPosition(...)). Keep seekable property/stream
and media controls gating. Add ExoPlayer seekable emission.
2026-03-09 07:21:36 +01:00
edde746 d08c4c905f fix: gamepad select registering as long press 2026-03-09 07:14:41 +01:00
edde746 43b9bd7038 fix: bump universal_gamepad to 1.5.4 2026-03-09 07:06:22 +01:00
edde746 2dddd00fd3 fix: d-pad navigation on Android 2026-03-09 06:01:15 +01:00
edde746 5be7b43cb8 fix: improve content strip text visibility
close #656
2026-03-09 01:04:14 +01:00
edde746 965d9540fa fix: double sparkle update check 2026-03-09 00:29:58 +01:00
edde746 65bb7bd44e fix: catch wakelock exceptions on Linux 2026-03-08 21:20:51 +01:00
edde746 a5ade78cd6 fix: remove unused variable warning 2026-03-08 19:40:11 +01:00
edde746 57420e51cf fix: lazy client fetch in context menu 2026-03-08 19:03:03 +01:00
edde746 bec8b593be fix: bump auto_updater 2026-03-08 18:50:19 +01:00
edde746 c62e406749 refactor: gate sentry behind build flag 2026-03-08 18:15:19 +01:00
github-actions[bot] d503f6c6ce chore: update cask to 1.25.0 2026-03-08 16:48:29 +00:00
github-actions[bot] ff6e3ee92a chore: bump version to 1.25.0 2026-03-08 16:24:11 +00:00
edde746 16676bca49 perf: watch together CPU overhead 2026-03-08 16:57:15 +01:00
edde746 9e2e1f72ce refactor: remove debug token i18n strings 2026-03-08 15:13:19 +01:00
edde746 f85afbe576 fix: gamepad ANR on Android 2026-03-08 14:15:32 +01:00
edde746 6fa6b34b59 fix: player stuck after show ends with no next episode
close #650
2026-03-08 13:51:25 +01:00
edde746 7a1f677c75 fix: content strip lint warnings 2026-03-07 22:10:45 +01:00
edde746 de28691481 fix: track selection persistence with stale cache 2026-03-07 22:07:29 +01:00
edde746 686a61ac74 fix: hero section bottom fade only applies to image 2026-03-07 20:52:28 +01:00
edde746 0b1f64ef8a refactor: simplify mobile player controls
close #641
2026-03-07 20:48:17 +01:00
edde746 5b910fd3d7 fix: mpv config TextField dpad/back key navigation
Add PopScope to handle Android TV platform-level back, add down arrow
exit on last line, and fix back key trap when save preset is disabled.
2026-03-07 20:32:02 +01:00
edde746 c56e73eefe feat: add content strip for dpad navigation 2026-03-07 20:17:28 +01:00
edde746 7cfa18e1e9 fix: remove redundant thumbnail popup during dpad timeline seeking 2026-03-07 18:29:52 +01:00
edde746 06034bb8e7 feat: scale queue items for tablet screens
close #644
2026-03-07 17:06:13 +01:00
edde746 7b20543fa1 fix: support non-1x playback speed in audio position bypass
Scale raw AudioTrack position by playback speed to convert from real
time to media time. Captures reference points on speed changes for
correct mid-playback transitions.
2026-03-07 16:40:48 +01:00
edde746 d207185130 fix: bypass DefaultAudioSink position clamp for large audio frames
Fixes video stutter on files with 120ms Opus audio frames (e.g. Amlogic
S905X4). DefaultAudioSink clamps getCurrentPositionUs to writtenDuration
which lags between bursts, creating a sawtooth pattern that drops video
frames. PositionFixAudioSink bypasses the clamp using the raw AudioTrack
position when the delegate falls behind.
2026-03-07 16:27:29 +01:00
edde746 66628e1bca feat: add pt, ja, ru, pl, da, nb translations 2026-03-06 22:34:45 +01:00
edde746 beff4f9ca0 feat: add watch together playback rejoin flow
close #639
2026-03-06 20:42:18 +01:00
edde746 1318c54c0c feat: improve performance overlay layout and add decoder details 2026-03-06 19:25:41 +01:00
edde746 b05d69b10f fix: filter hub detail view by library section
close #638
2026-03-06 18:36:45 +01:00
edde746 7825490b0f fix: filter Windows file-lock cache manager errors from Sentry 2026-03-06 18:30:47 +01:00
edde746 c06b0efedc fix: correct linux package license metadata 2026-03-06 15:07:48 +01:00
edde746 43f4736936 fix: reopen ExoPlayer when tunneling mode changes 2026-03-06 13:49:03 +01:00
edde746 cf8f1849aa fix: harden watch together teardown sync 2026-03-06 12:52:28 +01:00
edde746 c9a1bc97c4 fix: remove dead state paths and restore track hotkeys 2026-03-06 11:50:45 +01:00
edde746 581f665f58 fix: restore iOS inline player after PiP closes 2026-03-06 11:35:25 +01:00
edde746 7b3a8ad642 fix: guard missing client in season detail screen 2026-03-06 11:09:42 +01:00
edde746 c1e2bc36ea refactor: share Apple MPV core and media helpers 2026-03-06 10:16:33 +01:00
edde746 72aed76a1c fix: remove unused _applyFilterWithFallback 2026-03-06 07:55:59 +01:00
edde746 7ae293631a fix: trust server-computed subtitle selection flags
The Plex server already applies all preference levels (account,
show/season, per-item) into stream `selected` flags. Remove redundant
client-side subtitleMode/account prefs logic and show/season metadata
fetching (2 fewer API calls per episode).
2026-03-06 04:25:47 +01:00
edde746 f3d24866b7 fix: trust Plex stream selection for subtitle defaults 2026-03-06 03:43:10 +01:00
edde746 985ba5d2c7 fix: toggle PiP button closes PiP when already active 2026-03-06 03:08:28 +01:00
edde746andGitHub f7ca80de77 Merge pull request #637 from micahmo/fix/setting-episode-artwork
Fix an issue setting episode artwork
2026-03-06 03:08:20 +01:00
Micah Morrison a9c7130d55 Fix an issue setting episode artwork 2026-03-05 19:21:30 -05:00
edde746 9730ffd541 fix: coerce subtitleMode string from Plex API to num 2026-03-05 21:53:52 +01:00
edde746 232f018ae7 fix: report JSON deserialization errors 2026-03-05 21:00:52 +01:00
edde746 8571ee74c4 fix: use addedAt for unwatched on-deck sort fallback
close #636
2026-03-05 19:57:36 +01:00
edde746 9566e0b897 perf: parallelize show/season metadata fetches 2026-03-05 18:56:52 +01:00
edde746 863f90f65d fix: respect series-level subtitle mode for episodes 2026-03-05 18:55:44 +01:00
edde746 22a733280a fix: respect view mode in detail screens, disable scale in list mode
close #631
2026-03-05 17:38:38 +01:00
edde746 a4008d3d6b fix: move mpv operations off UI thread 2026-03-05 16:55:44 +01:00
edde746 819d7b0f8f fix: scope library display settings per user profile
close #630
2026-03-05 16:42:13 +01:00
edde746 c785ce1f88 fix: queue sheet image loading priority with itemExtent 2026-03-05 16:11:14 +01:00
edde746 0c3588210a fix: pause hero auto-scroll on desktop background transition 2026-03-05 15:36:35 +01:00
edde746 db57fa73ce feat: add option to hide bottom navigation bar labels 2026-03-05 15:05:48 +01:00
edde746 0269825179 fix: windows single instance mutex 2026-03-05 14:59:40 +01:00
edde746 588b413223 fix: browse tab scrolling after alpha jump libraries 2026-03-05 14:41:21 +01:00
edde746 a2cd3397c9 fix: desktop context menu position and bottom sheet sizing 2026-03-05 14:25:23 +01:00
edde746 268f293704 refactor: deduplicate Dolby Vision conversion code
- Extract shared processNal() for NAL classification/conversion logic
- Share DoviExtractorOutputWrapper between MKV and MP4 paths
- Deduplicate DV profile codec queries via deviceSupportsDvProfile()
- Collapse repeated doviTrackOutput access chain in stats
2026-03-05 12:02:33 +01:00
edde746 317d5dbdd9 feat: DV Profile 7→8.1 RPU conversion via libdovi
Convert Dolby Vision Profile 7 RPU NALUs to Profile 8.1 using libdovi
native bridge, preserving dynamic tone mapping on devices with DV8
decoders. Falls back to HEVC stripping when no DV8 decoder is present.

- Add JNI bridge (dovi_bridge.cpp) wrapping libdovi's RPU converter
- Download prebuilt libdovi from edde746/libdovi-builds releases
- DoviConvertingTrackOutput: DV81 mode converts RPUs, HEVC_STRIP strips
- Retry logic: if native DV7 decoding fails, auto-retry with conversion
- Performance overlay shows active DV conversion mode
2026-03-05 11:11:57 +01:00
edde746 7f2d548d32 fix: item 0 steals focus after sidenav return 2026-03-04 18:39:49 +01:00
edde746 5877673755 fix: reduce UI padding between media items
close #629
2026-03-04 18:36:12 +01:00
edde746 3b351d7634 fix: iOS OOM during PiP playback cycles
Cancel leaked stream subscriptions in video player dispose and flush
sample buffer layer on PiP teardown/cleanup to release video frames.
2026-03-04 17:34:30 +01:00
edde746 e6ee83d9c2 fix: preserve home screen scroll position on back navigation 2026-03-04 16:34:36 +01:00
edde746 8b57e810ab feat: detail screen context menu button 2026-03-04 15:11:46 +01:00
edde746 4121d568c8 fix: sanitize mpv event channel strings for valid UTF-8 2026-03-04 14:59:11 +01:00
edde746 551bd83ffe fix: player disposed race condition 2026-03-04 14:37:51 +01:00
edde746 5f4e4bfdd3 fix: ExoPlayer fallback for unsupported DV formats
- Detect decoder hangs via AnalyticsListener (onVideoDecoderInitialized
  → onRenderedFirstFrame gap, confirmed by DecoderCounters)
- Detect deselected video tracks and trigger immediate MPV fallback
- Use reinhard tone-mapping on GPUs without compute shaders
2026-03-04 13:18:40 +01:00
edde746 8cd48f3af5 fix: firstClient null check race 2026-03-04 10:01:40 +01:00
edde746 13b5d7bae8 feat: secondary subtitle tracks
close #625
2026-03-04 09:35:36 +01:00
edde746 3a76754af9 feat: custom GLSL shader import
close #624
2026-03-04 08:47:40 +01:00
edde746 73eeac35a8 fix: disable async codec queueing 2026-03-04 07:53:13 +01:00
edde746 18abcef8a2 fix: offload large UTF-8 decoding to background isolate
Prevents ANR when Dio's responseDecoder synchronously decodes large API
responses on the main thread. Responses >50KB use compute(). Also caches
MediaCodecList query results in ExoPlayerCore companion object.
2026-03-04 07:42:26 +01:00
edde746 85800abf9f fix: disable tunneling for video codecs without tunneling support 2026-03-04 07:27:02 +01:00
edde746 2b72327013 fix: hide metal layer when window is occluded 2026-03-04 05:06:25 +01:00
edde746 cc86afb31d fix: sparkle update detection and startup hang 2026-03-04 01:09:35 +01:00
edde746 efac1a9f28 fix: watch together stream use after close 2026-03-04 00:20:48 +01:00
edde746 a9e4091500 fix: disable http breadcrumbs for sentry 2026-03-04 00:02:51 +01:00
edde746 891c4f8c32 fix: reduce currently-airing program contrast
close #622
2026-03-03 23:40:07 +01:00
edde746 96f7bd431c fix: ExoPlayer logs never reaching Dart log output
setLogLevel silently no-oped because initialized was false at call time.
emitLog gated all levels behind debugLoggingEnabled which was never set.
2026-03-03 23:23:06 +01:00
edde746 cd2ab9d9cd refactor: deduplicate shared patterns, replace hardcoded colors 2026-03-03 22:43:00 +01:00
edde746 8f3d52142d fix: watch together sync bugs and code quality
- Reset _firstPlayCompleted on detach so episode switches wait for peers
- Remove duplicate host join announcement from sync manager
- Use dedicated isPlaying field in sessionConfig with legacy fallback
- Replace Future.delayed with cancellable Timer to prevent sync indicator flicker
- Make Participant fields final and constructor const
2026-03-03 21:41:32 +01:00
edde746 cb7de47aed fix: guard ScrollController.position access in HorizontalScrollWithArrows
Prevents "Too many elements" StateError on Android TV when the controller
temporarily has multiple positions during widget rebuilds.
2026-03-03 20:45:38 +01:00
edde746 174c78bf67 fix: handle connectivity_plus PlatformException on Windows 2026-03-03 20:13:17 +01:00
edde746 19c36ca340 fix: normalize Sentry release version across platforms 2026-03-03 20:02:07 +01:00
github-actions[bot] b0432ece86 chore: update cask to 1.24.1 2026-03-03 18:39:39 +00:00
github-actions[bot] a009c6db93 chore: bump version to 1.24.1 2026-03-03 18:18:29 +00:00
edde746 e60b8d13b9 fix: improve error reporting data sanitization 2026-03-03 19:16:33 +01:00
edde746 1b673743b2 fix: handle int end-file reason from mpv on Windows/Linux 2026-03-03 19:00:32 +01:00
edde746 34e8c179fe fix: guard PiP calls on unsupported platforms 2026-03-03 19:00:32 +01:00
edde746 33d5760930 fix: uncommend play store release 2026-03-03 19:00:32 +01:00
github-actions[bot] 77ec4cc5c8 chore: update cask to 1.24.0 2026-03-03 17:02:12 +00:00
edde746 c8e6cac00c bump mpv windows to 20260303 2026-03-03 17:18:57 +01:00
github-actions[bot] 35fc0334bc chore: bump version to 1.24.0 2026-03-03 16:08:36 +00:00
edde746 c6ace690cb refactor: clean up download code duplication 2026-03-03 16:31:02 +01:00
edde746 7a979bed2f fix: harden download callbacks against stale status events
Guard _onDownloadFailed, _onDownloadPermanentlyFailed, and cancel
handler against overwriting completed downloads. Add
resumeFromBackground() for iOS background transitions.
2026-03-03 15:20:13 +01:00
edde746 e46bd71fa7 fix: prevent completed downloads from restarting on app launch
Race condition between trackTasks() completion callbacks and the orphan
scan caused completed items to be re-queued on every app restart (#616).
2026-03-03 15:02:40 +01:00
edde746 c98010234f fix: warnings 2026-03-03 14:30:59 +01:00
edde746 02e175e4d3 feat: replace rustrak with glitchtip 2026-03-03 14:27:11 +01:00
edde746 013bcfbece fix: library continue watching hidden
close #615
2026-03-03 12:17:37 +01:00
edde746 ebf2d1e516 fix: shorten nav labels
close #612
2026-03-03 12:13:43 +01:00
edde746 4bce668df6 fix: asymmetric divider in track sheet on landscape 2026-03-03 12:04:03 +01:00
edde746 c64c7defc3 fix: disable session tracking 2026-03-03 11:05:54 +01:00
edde746 1629801074 feat: add swipe-up content strip for mobile video controls 2026-03-03 11:03:31 +01:00
edde746 cd36e3acb0 feat: switch crash reporting from BugSink to Rustrak 2026-03-03 09:19:25 +01:00
edde746 7001032e51 feat: combine audio & subtitle track sheets into one 2026-03-03 07:16:45 +01:00
edde746 3a0bac4446 docs: add winget install option to readme 2026-03-03 06:35:15 +01:00
edde746 1c31f0effa fix: request audio focus before player init guard 2026-03-02 23:12:00 +01:00
edde746 01e61c590a fix: tighten startup connection timeouts for dead servers 2026-03-02 23:09:09 +01:00
edde746 a00e2b78d6 feat: replace mpv config UI with raw text editor 2026-03-02 23:06:05 +01:00
edde746 b5904bc477 fix: auto-retry large downloads after native retries exhausted
Increase native retries 3→5 and add app-level auto-retry (3 rounds,
30s delay) so large downloads that fail with "unexpected end of stream"
recover automatically. 404s fail immediately without retry.
2026-03-02 21:43:01 +01:00
edde746 47f2afb665 feat: bottom sheet polish 2026-03-02 21:24:53 +01:00
edde746 3b2e278868 fix: hide redundant items in context menu 2026-03-02 20:42:47 +01:00
edde746 8baa84c06f fix: use player.state.track for selected subtitle check 2026-03-02 19:25:13 +01:00
edde746 92b589213f fix(ci): add libcurl4-openssl-dev for Linux builds 2026-03-02 19:21:26 +01:00
edde746 94395feaba feat: implement sub-visibility toggle
Closes #597
2026-03-02 19:07:56 +01:00
edde746 c372920965 fix: resolve out-of-scope settingsService in video player 2026-03-02 18:26:05 +01:00
edde746 5ab6af1b02 feat: respect system 24h time format setting
Closes #605
2026-03-02 18:19:52 +01:00
edde746 34252b9ba7 fix: persist aspect ratio and shader settings
Closes #602, closes #603
2026-03-02 18:19:46 +01:00
edde746 8d3f448fdf feat(macos): add picture-in-picture support 2026-03-02 18:19:36 +01:00
edde746 7ebbfb9ef7 fix(i18n): add missing pip keys 2026-03-02 14:31:40 +01:00
edde746 a008f86ee2 feat(ios): add picture-in-picture support
close #44
2026-03-02 14:06:11 +01:00
edde746 5f1c51ff6c fix: skip specials in playback order
close #601
2026-03-02 07:32:20 +01:00
edde746 229d6b24e1 feat: crash log reporting 2026-03-02 07:29:21 +01:00
edde746 9dc0654225 fix(android): prevent dispatchWindowVisibilityChanged NPE during disposal 2026-03-02 06:10:00 +01:00
edde746 36277076bd fix(android): cap demuxer buffers + sync mpv dispose 2026-03-02 02:30:48 +01:00
edde746 f96ef0740d fix(android): move JSON parsing off main thread 2026-03-02 02:24:08 +01:00
edde746 6bee1a6c38 fix(android): add opensles audio fallback 2026-03-02 02:05:24 +01:00
edde746 0dbba6439d fix: hide hero carousel indicators in dpad mode
close #600
2026-03-02 01:50:51 +01:00
edde746 7ac888cd46 fix(windows): installer norun arg 2026-03-01 20:10:03 +01:00
github-actions[bot] 88f4ae9987 chore: update cask to 1.23.0 2026-03-01 16:11:23 +00:00
edde746 938291dbf6 bump mpvkit 2026-03-01 16:55:59 +01:00
edde746 f5715987fc feat: migrate iOS to scene lifecycle 2026-03-01 15:06:45 +01:00
edde746 7c0e86c3cf bump mpvkit 2026-03-01 13:59:44 +01:00
406 changed files with 65996 additions and 20906 deletions
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -uo pipefail
if [ "${SKIP_HOOKS:-}" = "1" ]; then
echo "[pre-commit] skipped (SKIP_HOOKS=1)"
exit 0
fi
GIT_DIR="$(git rev-parse --git-dir)"
if [ -f "$GIT_DIR/MERGE_MSG" ] || [ -f "$GIT_DIR/MERGE_HEAD" ] || [ -f "$GIT_DIR/REVERT_HEAD" ]; then
echo "[pre-commit] skipped (merge/revert in progress)"
exit 0
fi
if git diff --cached --quiet; then
echo "[pre-commit] nothing staged — skipping checks"
exit 0
fi
ROOT="$(git rev-parse --show-toplevel)"
exec "$ROOT/scripts/ci_checks.sh"
+1
View File
@@ -1 +1,2 @@
github: edde746
liberapay: edde746
+22 -12
View File
@@ -3,6 +3,12 @@ name: Build
on:
workflow_dispatch:
env:
SENTRY_DART_DEFINE: ${{ github.repository == 'edde746/plezy' && '--dart-define=ENABLE_SENTRY=true' || '' }}
GIT_COMMIT_DART_DEFINE: --dart-define=GIT_COMMIT=${{ github.sha }}
SENTRY_ENV_DART_DEFINE: --dart-define=SENTRY_ENVIRONMENT=github
DONATIONS_DART_DEFINE: --dart-define=ENABLE_DONATIONS=true
jobs:
build-android:
runs-on: ubuntu-latest
@@ -58,7 +64,7 @@ jobs:
EOF
- name: Build APKs
run: flutter build apk --release --split-per-abi --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build apk --release --split-per-abi --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
- name: Clean up keystore
if: always()
@@ -126,7 +132,7 @@ jobs:
run: flutter pub get
- name: Build iOS (no codesign)
run: flutter build ios --release --no-codesign --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build ios --release --no-codesign --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
- name: Create IPA
run: |
@@ -184,7 +190,7 @@ jobs:
run: flutter pub get
- name: Build macOS
run: flutter build macos --release --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build macos --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
- name: Import Code Signing Certificate
env:
@@ -335,7 +341,7 @@ jobs:
run: flutter pub get
- name: Build Windows x64
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
- name: Upload x64 build
uses: actions/upload-artifact@v4
@@ -372,7 +378,7 @@ jobs:
run: flutter pub get
- name: Build Windows arm64
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
- name: Upload arm64 build
uses: actions/upload-artifact@v4
@@ -490,13 +496,13 @@ jobs:
- name: Cache APT packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
version: 1.1
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
- name: Cache libmpv build
id: libmpv-cache
@@ -516,7 +522,7 @@ jobs:
run: flutter pub get
- name: Build Linux x64
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/x86_64-linux-gnu/pkgconfig
@@ -598,13 +604,13 @@ jobs:
- name: Cache APT packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
version: 1.1
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
- name: Cache libmpv build
id: libmpv-cache
@@ -624,7 +630,7 @@ jobs:
run: flutter pub get
- name: Build Linux arm64
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/aarch64-linux-gnu/pkgconfig
@@ -712,11 +718,14 @@ jobs:
id: version
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//')
BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Generate appcast.xml
run: |
VERSION="${{ steps.version.outputs.version }}"
BUILD_NUMBER="${{ steps.version.outputs.build_number }}"
# Read macOS signature metadata (may be empty if secrets not configured)
MACOS_ED_SIG=""
@@ -745,7 +754,8 @@ jobs:
<title>Plezy Updates</title>
<item>
<title>Version ${VERSION}</title>
<sparkle:version>${VERSION}</sparkle:version>
<sparkle:version>${BUILD_NUMBER}</sparkle:version>
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
<sparkle:releaseNotesLink>https://gh-html.edde746.dev/edde746/plezy/${VERSION}</sparkle:releaseNotesLink>
XMLEOF
+24
View File
@@ -52,3 +52,27 @@ jobs:
identifier: edde746.Plezy
installers-regex: 'plezy-windows-installer\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
update-appcast-branch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download appcast.xml from release
run: |
gh release download "${{ github.event.release.tag_name }}" \
--pattern "appcast.xml"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push to appcast branch
run: |
VERSION="${{ github.event.release.tag_name }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BLOB=$(git hash-object -w appcast.xml)
TREE=$(printf "100644 blob %s\tappcast.xml\n" "$BLOB" | git mktree)
COMMIT=$(git commit-tree "$TREE" -m "Update appcast for $VERSION")
git push origin "$COMMIT:refs/heads/appcast" --force
+2 -2
View File
@@ -1,6 +1,6 @@
cask "plezy" do
version "1.22.0"
sha256 "94524bb6b7c4779f5416cef7d3a876e081f7ad6e737e828f790e52920b7086f4"
version "1.33.1"
sha256 "155722eb653c0efa939e47c38d1fb340c693f9039424effcb9c2bf4aea80720e"
url "https://github.com/edde746/plezy/releases/download/#{version}/plezy-macos.dmg"
name "Plezy"
+9 -1
View File
@@ -20,12 +20,20 @@ A modern Plex client for desktop and mobile. Built with Flutter for native perfo
- [Windows (x64, arm64)](https://github.com/edde746/plezy/releases/latest/download/plezy-windows-installer.exe)
- [macOS (x64, arm64)](https://github.com/edde746/plezy/releases/latest/download/plezy-macos.dmg)
- [Linux (x64, arm64)](https://github.com/edde746/plezy/releases/latest) - .deb, .rpm, .pkg.tar.zst, and portable tar.gz available
- [NixOS/Nix](https://github.com/mio-19/nurpkgs/tree/main/pkgs/plezy) - Community package by [@mio-19](https://github.com/mio-19)
- [Nix](https://search.nixos.org/packages?channel=unstable&query=plezy) - Community package by [@mio-19](https://github.com/mio-19) and [@MiniHarinn](https://github.com/MiniHarinn)
- **Homebrew** (macOS):
```bash
brew tap edde746/plezy https://github.com/edde746/plezy
brew install --cask plezy
```
- [AUR](https://aur.archlinux.org/packages/plezy-bin) (Arch Linux) - Community maintained by [@jianglai](https://github.com/jianglai):
```bash
yay -S plezy-bin
```
- **WinGet** (Windows):
```bash
winget install edde746.plezy
```
## Features
+103 -5
View File
@@ -8,6 +8,66 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val mpvVersion = "v1.0.6"
val mpvDir = layout.buildDirectory.dir("libmpv").get().asFile
val mpvAar = "libmpv-release.aar"
val downloadLibmpv by tasks.registering {
val stamp = File(mpvDir, ".version")
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == mpvVersion }
doLast {
mpvDir.mkdirs()
val url = "https://github.com/edde746/libmpv-android/releases/download/$mpvVersion/$mpvAar"
exec { commandLine("curl", "-sfL", url, "-o", File(mpvDir, mpvAar).absolutePath) }
stamp.writeText(mpvVersion)
}
}
val assVersion = "fp-2"
val assDir = layout.buildDirectory.dir("libass").get().asFile
val assAars = listOf("lib_ass-release.aar", "lib_ass_kt-release.aar", "lib_ass_media-release.aar")
val downloadLibass by tasks.registering {
val stamp = File(assDir, ".version")
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == assVersion }
doLast {
assDir.mkdirs()
val baseUrl = "https://github.com/edde746/libass-android/releases/download/$assVersion"
assAars.forEach { name ->
val dest = File(assDir, name)
exec { commandLine("curl", "-sfL", "$baseUrl/$name", "-o", dest.absolutePath) }
}
stamp.writeText(assVersion)
}
}
val doviVersion = "2.3.1"
val doviDir = layout.buildDirectory.dir("libdovi").get().asFile
val doviAbis = mapOf(
"arm64-v8a" to "aarch64-linux-android",
"armeabi-v7a" to "armv7-linux-androideabi",
"x86" to "i686-linux-android",
"x86_64" to "x86_64-linux-android",
)
val downloadLibdovi by tasks.registering {
val stamp = File(doviDir, ".version")
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == doviVersion }
doLast {
doviDir.mkdirs()
val baseUrl = "https://github.com/edde746/libdovi-builds/releases/download/v$doviVersion"
doviAbis.forEach { (abi, triple) ->
val archive = File(doviDir, "$triple.tar.gz")
exec { commandLine("curl", "-sfL", "$baseUrl/libdovi-$triple.tar.gz", "-o", archive.absolutePath) }
val outDir = File(doviDir, "$abi/lib")
outDir.mkdirs()
exec { commandLine("tar", "-xzf", archive.absolutePath, "-C", outDir.absolutePath) }
archive.delete()
}
stamp.writeText(doviVersion)
}
}
android {
namespace = "com.edde746.plezy"
compileSdk = flutter.compileSdkVersion
@@ -30,6 +90,28 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
externalNativeBuild {
cmake {
arguments += listOf(
"-DDOVI_ENABLE_LIBDOVI=ON",
"-DDOVI_LIBDOVI_PREBUILT_ROOT=${doviDir.absolutePath}"
)
}
}
if (System.getenv("AMAZON") != null) {
versionCode = (flutter.versionCode ?: 0) + 3000
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
signingConfigs {
@@ -73,22 +155,38 @@ flutter {
source = "../.."
}
// Download libdovi before any CMake/native build task
tasks.matching { it.name.contains("CMake") || it.name.contains("externalNative") }.configureEach {
dependsOn(downloadLibdovi)
}
// Download libmpv and libass AARs before compilation
tasks.matching { it.name.startsWith("pre") && it.name.endsWith("Build") }.configureEach {
dependsOn(downloadLibmpv)
dependsOn(downloadLibass)
}
dependencies {
implementation("dev.jdtech.mpv:libmpv:0.5.1")
implementation(files(File(mpvDir, mpvAar)))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
// Android TV Watch Next integration
implementation("androidx.tvprovider:tvprovider:1.0.0")
// Media3 ExoPlayer for Android
implementation("androidx.media3:media3-exoplayer:1.9.2")
implementation("androidx.media3:media3-exoplayer-hls:1.9.2")
implementation("androidx.media3:media3-ui:1.9.2")
implementation("androidx.media3:media3-common:1.9.2")
// Cronet for HTTP/2 multiplexing + better connection management
implementation("androidx.media3:media3-datasource-cronet:1.9.2")
implementation("org.chromium.net:cronet-embedded:143.7445.0")
// FFmpeg audio decoder for unsupported codecs (ALAC, DTS, TrueHD, etc.)
implementation("org.jellyfin.media3:media3-ffmpeg-decoder:1.9.0+1")
// libass-android for ASS/SSA subtitle rendering (fork with font buffering fix)
implementation(files("libs/ass-media.aar"))
// ass-kt core library (needed for AssRender.setFontScale)
implementation("io.github.peerless2012:ass-kt:0.4.0-beta01")
// libass-android for ASS/SSA subtitle rendering
assAars.forEach { implementation(files(File(assDir, it))) }
}
Binary file not shown.
+2 -7
View File
@@ -6,14 +6,8 @@
<!-- Internet access permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Camera permission for QR code scanning (companion remote pairing) -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- PIP Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<!-- Media session permissions for OS media controls -->
<!-- PIP and media session foreground service permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
@@ -27,6 +21,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
<!-- Android TV support (not required, but allow detection) -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<!-- Android TV Watch Next integration -->
+19
View File
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.22.1)
project(dovi_bridge)
option(DOVI_ENABLE_LIBDOVI "Link real libdovi" ON)
set(DOVI_LIBDOVI_PREBUILT_ROOT "" CACHE PATH "Path to prebuilt libdovi")
add_library(dovi_bridge SHARED dovi_bridge.cpp)
if(DOVI_ENABLE_LIBDOVI)
add_library(dovi_static STATIC IMPORTED)
set_target_properties(dovi_static PROPERTIES
IMPORTED_LOCATION "${DOVI_LIBDOVI_PREBUILT_ROOT}/${ANDROID_ABI}/lib/libdovi.a")
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=1)
target_include_directories(dovi_bridge PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(dovi_bridge dovi_static log)
else()
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=0)
target_link_libraries(dovi_bridge log)
endif()
+126
View File
@@ -0,0 +1,126 @@
#include <jni.h>
#include <android/log.h>
#include <cstring>
#include <new>
#define TAG "DoviBridge"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#if DOVI_REAL_LINKED
#include "include/libdovi/rpu_parser.h"
#endif
static const char* BRIDGE_VERSION = "1.0.0";
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
JNIEnv *env, jclass, jbyteArray payload, jint mode) {
#if !DOVI_REAL_LINKED
return nullptr;
#else
if (payload == nullptr) return nullptr;
jsize len = env->GetArrayLength(payload);
if (len <= 0) return nullptr;
// Valid RPU NALs are typically <2 KiB; reject unreasonable sizes
if (len > 8192) {
LOGW("RPU payload too large (%d bytes), skipping", len);
return nullptr;
}
// Copy to native heap so libdovi never touches JVM heap memory.
// GetByteArrayElements on ART may return a direct heap pointer; any
// out-of-bounds access by libdovi would corrupt adjacent JVM objects.
auto *buf = new (std::nothrow) uint8_t[static_cast<size_t>(len)];
if (buf == nullptr) return nullptr;
env->GetByteArrayRegion(payload, 0, len, reinterpret_cast<jbyte *>(buf));
if (env->ExceptionCheck()) {
delete[] buf;
return nullptr;
}
// Try dovi_parse_unspec62_nalu first (handles escaped NALs), fallback to dovi_parse_rpu
DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(buf, static_cast<size_t>(len));
if (rpu == nullptr) {
delete[] buf;
return nullptr;
}
const char *err = dovi_rpu_get_error(rpu);
if (err != nullptr) {
// Fallback: try dovi_parse_rpu (raw RPU without NAL framing)
dovi_rpu_free(rpu);
rpu = dovi_parse_rpu(buf, static_cast<size_t>(len));
if (rpu == nullptr) {
delete[] buf;
return nullptr;
}
err = dovi_rpu_get_error(rpu);
if (err != nullptr) {
LOGW("RPU parse failed: %s", err);
dovi_rpu_free(rpu);
delete[] buf;
return nullptr;
}
}
delete[] buf;
// Convert to target profile (mode 2 = P8.1 with no-op curves)
int32_t ret = dovi_convert_rpu_with_mode(rpu, static_cast<uint8_t>(mode));
if (ret != 0) {
err = dovi_rpu_get_error(rpu);
LOGW("RPU conversion failed (mode %d): %s", mode, err ? err : "unknown");
dovi_rpu_free(rpu);
return nullptr;
}
// Write back as UNSPEC62 NAL
const DoviData *out = dovi_write_unspec62_nalu(rpu);
if (out == nullptr || out->data == nullptr || out->len == 0) {
err = dovi_rpu_get_error(rpu);
LOGW("RPU write failed: %s", err ? err : "unknown");
if (out != nullptr) dovi_data_free(out);
dovi_rpu_free(rpu);
return nullptr;
}
if (out->len > 16384) {
LOGW("RPU output unexpectedly large (%zu bytes), discarding", out->len);
dovi_data_free(out);
dovi_rpu_free(rpu);
return nullptr;
}
jbyteArray result = env->NewByteArray(static_cast<jsize>(out->len));
if (result != nullptr) {
env->SetByteArrayRegion(result, 0, static_cast<jsize>(out->len),
reinterpret_cast<const jbyte *>(out->data));
}
dovi_data_free(out);
dovi_rpu_free(rpu);
return result;
#endif
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady(
JNIEnv *, jclass) {
#if DOVI_REAL_LINKED
return JNI_TRUE;
#else
return JNI_FALSE;
#endif
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeGetBridgeVersion(
JNIEnv *env, jclass) {
return env->NewStringUTF(BRIDGE_VERSION);
}
@@ -0,0 +1,747 @@
// SPDX-License-Identifier: MIT
#ifndef DOVI_H
#define DOVI_H
#define RPU_PARSER_MAJOR 3
#define RPU_PARSER_MINOR 3
#define RPU_PARSER_PATCH 2
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#define DoviNUM_COMPONENTS 3
/**
* Opaque Dolby Vision RPU.
*
* Use dovi_rpu_free to free.
* It should be freed regardless of whether or not an error occurred.
*/
typedef struct DoviRpuOpaque DoviRpuOpaque;
/**
* Struct representing a data buffer
*/
typedef struct {
/**
* Pointer to the data buffer
*/
const uint8_t *data;
/**
* Data buffer size
*/
size_t len;
} DoviData;
/**
* C struct for rpu_data_header()
*/
typedef struct {
/**
* Profile guessed from the values in the header
*/
uint8_t guessed_profile;
/**
* Enhancement layer type (FEL or MEL) if the RPU is profile 7
* null pointer if not profile 7
*/
const char *el_type;
/**
* Deprecated since 3.2.0
* The field is not actually part of the RPU header
*/
uint8_t rpu_nal_prefix;
uint8_t rpu_type;
uint16_t rpu_format;
uint8_t vdr_rpu_profile;
uint8_t vdr_rpu_level;
bool vdr_seq_info_present_flag;
bool chroma_resampling_explicit_filter_flag;
uint8_t coefficient_data_type;
uint64_t coefficient_log2_denom;
uint8_t vdr_rpu_normalized_idc;
bool bl_video_full_range_flag;
uint64_t bl_bit_depth_minus8;
uint64_t el_bit_depth_minus8;
uint64_t vdr_bit_depth_minus8;
bool spatial_resampling_filter_flag;
uint8_t reserved_zero_3bits;
bool el_spatial_resampling_filter_flag;
bool disable_residual_flag;
bool vdr_dm_metadata_present_flag;
bool use_prev_vdr_rpu_flag;
uint64_t prev_vdr_rpu_id;
} DoviRpuDataHeader;
/**
* Struct representing a data buffer
*/
typedef struct {
/**
* Pointer to the data buffer. Can be null if length is zero.
*/
const uint16_t *data;
/**
* Data buffer size
*/
size_t len;
} DoviU16Data;
/**
* Struct representing a data buffer
*/
typedef struct {
/**
* Pointer to the data buffer. Can be null if length is zero.
*/
const uint64_t *data;
/**
* Data buffer size
*/
size_t len;
} DoviU64Data;
/**
* Struct representing a data buffer
*/
typedef struct {
/**
* Pointer to the data buffer
*/
const int64_t *data;
/**
* Data buffer size
*/
size_t len;
} DoviI64Data;
/**
* Struct representing a 2D data buffer
*/
typedef struct {
/**
* Pointer to the list of Data structs
*/
const DoviI64Data *const *list;
/**
* List length
*/
size_t len;
} DoviI64Data2D;
/**
* Struct representing a 2D data buffer
*/
typedef struct {
/**
* Pointer to the list of Data structs
*/
const DoviU64Data *const *list;
/**
* List length
*/
size_t len;
} DoviU64Data2D;
typedef struct {
DoviU64Data poly_order_minus1;
DoviData linear_interp_flag;
DoviI64Data2D poly_coef_int;
DoviU64Data2D poly_coef;
} DoviPolynomialCurve;
/**
* Struct representing a 3D data buffer
*/
typedef struct {
/**
* Pointer to the list of I64Data2D structs
*/
const DoviI64Data2D *const *list;
/**
* List length
*/
size_t len;
} DoviI64Data3D;
/**
* Struct representing a 3D data buffer
*/
typedef struct {
/**
* Pointer to the list of U64Data2D structs
*/
const DoviU64Data2D *const *list;
/**
* List length
*/
size_t len;
} DoviU64Data3D;
typedef struct {
DoviData mmr_order_minus1;
DoviI64Data mmr_constant_int;
DoviU64Data mmr_constant;
DoviI64Data3D mmr_coef_int;
DoviU64Data3D mmr_coef;
} DoviMMRCurve;
typedef struct {
/**
* [2, 9]
*/
uint64_t num_pivots_minus2;
DoviU16Data pivots;
/**
* Consistent for a component
* Luma (component 0): Polynomial = 0
* Chroma (components 1 and 2): MMR = 1
*/
uint8_t mapping_idc;
/**
* mapping_idc = 0, null pointer otherwise
*/
const DoviPolynomialCurve *polynomial;
/**
* mapping_idc = 1, null pointer otherwise
*/
const DoviMMRCurve *mmr;
} DoviReshapingCurve;
/**
* C struct for rpu_data_nlq()
*/
typedef struct {
uint16_t nlq_offset[DoviNUM_COMPONENTS];
uint64_t vdr_in_max_int[DoviNUM_COMPONENTS];
uint64_t vdr_in_max[DoviNUM_COMPONENTS];
uint64_t linear_deadzone_slope_int[DoviNUM_COMPONENTS];
uint64_t linear_deadzone_slope[DoviNUM_COMPONENTS];
uint64_t linear_deadzone_threshold_int[DoviNUM_COMPONENTS];
uint64_t linear_deadzone_threshold[DoviNUM_COMPONENTS];
} DoviRpuDataNlq;
/**
* C struct for rpu_data_mapping()
*/
typedef struct {
uint64_t vdr_rpu_id;
uint64_t mapping_color_space;
uint64_t mapping_chroma_format_idc;
uint64_t num_x_partitions_minus1;
uint64_t num_y_partitions_minus1;
DoviReshapingCurve curves[DoviNUM_COMPONENTS];
/**
* Set to -1 to represent Option::None
*/
int32_t nlq_method_idc;
/**
* Set to -1 to represent Option::None
*/
int32_t nlq_num_pivots_minus2;
/**
* Length of zero when not present. Only present in profile 4 and 7.
*/
DoviU16Data nlq_pred_pivot_value;
/**
* Pointer to `RpuDataNlq` struct, null if not dual layer profile
*/
const DoviRpuDataNlq *nlq;
} DoviRpuDataMapping;
/**
* Statistical analysis of the frame: min, max, avg brightness.
*/
typedef struct {
uint16_t min_pq;
uint16_t max_pq;
uint16_t avg_pq;
} DoviExtMetadataBlockLevel1;
/**
* Creative intent trim passes per target display peak brightness
*/
typedef struct {
uint16_t target_max_pq;
uint16_t trim_slope;
uint16_t trim_offset;
uint16_t trim_power;
uint16_t trim_chroma_weight;
uint16_t trim_saturation_gain;
int16_t ms_weight;
} DoviExtMetadataBlockLevel2;
typedef struct {
/**
* Pointer to the list of ExtMetadataBlockLevel2 structs
*/
const DoviExtMetadataBlockLevel2 *const *list;
/**
* List length
*/
size_t len;
} DoviLevel2BlockList;
/**
* Level 1 offsets.
*/
typedef struct {
uint16_t min_pq_offset;
uint16_t max_pq_offset;
uint16_t avg_pq_offset;
} DoviExtMetadataBlockLevel3;
/**
* Something about temporal stability
*/
typedef struct {
uint16_t anchor_pq;
uint16_t anchor_power;
} DoviExtMetadataBlockLevel4;
/**
* Active area of the picture (letterbox, aspect ratio)
*/
typedef struct {
uint16_t active_area_left_offset;
uint16_t active_area_right_offset;
uint16_t active_area_top_offset;
uint16_t active_area_bottom_offset;
} DoviExtMetadataBlockLevel5;
/**
* ST2086/HDR10 metadata fallback
*/
typedef struct {
uint16_t max_display_mastering_luminance;
uint16_t min_display_mastering_luminance;
uint16_t max_content_light_level;
uint16_t max_frame_average_light_level;
} DoviExtMetadataBlockLevel6;
/**
* Creative intent trim passes per target display peak brightness
* For CM v4.0, L8 metadata only is present and used to compute L2
*
* This block can have varying byte lengths: 10, 12, 13, 19, 25
* Depending on the length, the fields parsed default to zero and may not be set.
* Up to (including):
* - 10: ms_weight
* - 12: target_mid_contrast
* - 13: clip_trim
* - 19: saturation_vector_field[0-5]
* - 25: hue_vector_field[0-5]
*/
typedef struct {
uint64_t length;
uint8_t target_display_index;
uint16_t trim_slope;
uint16_t trim_offset;
uint16_t trim_power;
uint16_t trim_chroma_weight;
uint16_t trim_saturation_gain;
uint16_t ms_weight;
uint16_t target_mid_contrast;
uint16_t clip_trim;
uint8_t saturation_vector_field0;
uint8_t saturation_vector_field1;
uint8_t saturation_vector_field2;
uint8_t saturation_vector_field3;
uint8_t saturation_vector_field4;
uint8_t saturation_vector_field5;
uint8_t hue_vector_field0;
uint8_t hue_vector_field1;
uint8_t hue_vector_field2;
uint8_t hue_vector_field3;
uint8_t hue_vector_field4;
uint8_t hue_vector_field5;
} DoviExtMetadataBlockLevel8;
typedef struct {
/**
* Pointer to the list of ExtMetadataBlockLevel8 structs
*/
const DoviExtMetadataBlockLevel8 *const *list;
/**
* List length
*/
size_t len;
} DoviLevel8BlockList;
/**
* Source/mastering display color primaries
*
* This block can have varying byte lengths: 1 or 17
* Depending on the length, the fields parsed default to zero and may not be set.
* Up to (including):
* - 1: source_primary_index
* - 17: source_primary_{red,green,blue,white}_{x,y}
*/
typedef struct {
uint64_t length;
uint8_t source_primary_index;
uint16_t source_primary_red_x;
uint16_t source_primary_red_y;
uint16_t source_primary_green_x;
uint16_t source_primary_green_y;
uint16_t source_primary_blue_x;
uint16_t source_primary_blue_y;
uint16_t source_primary_white_x;
uint16_t source_primary_white_y;
} DoviExtMetadataBlockLevel9;
/**
* Custom target display information
*
* This block can have varying byte lengths: 5 or 21
* Depending on the length, the fields parsed default to zero and may not be set.
* Up to (including):
* - 5: target_primary_index
* - 21: target_primary_{red,green,blue,white}_{x,y}
*/
typedef struct {
uint64_t length;
uint8_t target_display_index;
uint16_t target_max_pq;
uint16_t target_min_pq;
uint8_t target_primary_index;
uint16_t target_primary_red_x;
uint16_t target_primary_red_y;
uint16_t target_primary_green_x;
uint16_t target_primary_green_y;
uint16_t target_primary_blue_x;
uint16_t target_primary_blue_y;
uint16_t target_primary_white_x;
uint16_t target_primary_white_y;
} DoviExtMetadataBlockLevel10;
typedef struct {
/**
* Pointer to the list of ExtMetadataBlockLevel10 structs
*/
const DoviExtMetadataBlockLevel10 *const *list;
/**
* List length
*/
size_t len;
} DoviLevel10BlockList;
/**
* Content type metadata level
*/
typedef struct {
uint8_t content_type;
uint8_t whitepoint;
bool reference_mode_flag;
uint8_t reserved_byte2;
uint8_t reserved_byte3;
} DoviExtMetadataBlockLevel11;
/**
* Metadata level present in CM v4.0
*/
typedef struct {
uint8_t dm_mode;
uint8_t dm_version_index;
} DoviExtMetadataBlockLevel254;
/**
* Metadata level optionally present in CM v2.9.
* Different display modes (calibration/verify/bypass), debugging
*/
typedef struct {
uint8_t dm_run_mode;
uint8_t dm_run_version;
uint8_t dm_debug0;
uint8_t dm_debug1;
uint8_t dm_debug2;
uint8_t dm_debug3;
} DoviExtMetadataBlockLevel255;
/**
* C struct for the list of ext_metadata_block()
*/
typedef struct {
/**
* Number of metadata blocks
*/
uint64_t num_ext_blocks;
const DoviExtMetadataBlockLevel1 *level1;
DoviLevel2BlockList level2;
const DoviExtMetadataBlockLevel3 *level3;
const DoviExtMetadataBlockLevel4 *level4;
const DoviExtMetadataBlockLevel5 *level5;
const DoviExtMetadataBlockLevel6 *level6;
DoviLevel8BlockList level8;
const DoviExtMetadataBlockLevel9 *level9;
DoviLevel10BlockList level10;
const DoviExtMetadataBlockLevel11 *level11;
const DoviExtMetadataBlockLevel254 *level254;
const DoviExtMetadataBlockLevel255 *level255;
} DoviDmData;
/**
* C struct for vdr_dm_data()
*/
typedef struct {
bool compressed;
uint64_t affected_dm_metadata_id;
uint64_t current_dm_metadata_id;
uint64_t scene_refresh_flag;
int16_t ycc_to_rgb_coef0;
int16_t ycc_to_rgb_coef1;
int16_t ycc_to_rgb_coef2;
int16_t ycc_to_rgb_coef3;
int16_t ycc_to_rgb_coef4;
int16_t ycc_to_rgb_coef5;
int16_t ycc_to_rgb_coef6;
int16_t ycc_to_rgb_coef7;
int16_t ycc_to_rgb_coef8;
uint32_t ycc_to_rgb_offset0;
uint32_t ycc_to_rgb_offset1;
uint32_t ycc_to_rgb_offset2;
int16_t rgb_to_lms_coef0;
int16_t rgb_to_lms_coef1;
int16_t rgb_to_lms_coef2;
int16_t rgb_to_lms_coef3;
int16_t rgb_to_lms_coef4;
int16_t rgb_to_lms_coef5;
int16_t rgb_to_lms_coef6;
int16_t rgb_to_lms_coef7;
int16_t rgb_to_lms_coef8;
uint16_t signal_eotf;
uint16_t signal_eotf_param0;
uint16_t signal_eotf_param1;
uint32_t signal_eotf_param2;
uint8_t signal_bit_depth;
uint8_t signal_color_space;
uint8_t signal_chroma_format;
uint8_t signal_full_range_flag;
uint16_t source_min_pq;
uint16_t source_max_pq;
uint16_t source_diagonal;
DoviDmData dm_data;
} DoviVdrDmData;
/**
* Heap allocated list of valid RPU pointers
*/
typedef struct {
DoviRpuOpaque *const *list;
size_t len;
const char *error;
} DoviRpuOpaqueList;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* # Safety
* The pointer to the data must be valid.
*
* Parse a Dolby Vision RPU from unescaped byte buffer.
* Adds an error if the parsing fails.
*/
DoviRpuOpaque *dovi_parse_rpu(const uint8_t *buf, size_t len);
/**
* # Safety
* The pointer to the data must be valid.
*
* Parse a Dolby Vision from a AV1 ITU-T T.35 metadata OBU byte buffer.
* Adds an error if the parsing fails.
*/
DoviRpuOpaque *dovi_parse_itu_t35_dovi_metadata_obu(const uint8_t *buf, size_t len);
/**
* # Safety
* The pointer to the data must be valid.
*
* Parse a Dolby Vision from a (possibly) escaped HEVC UNSPEC 62 NAL unit byte buffer.
* Adds an error if the parsing fails.
*/
DoviRpuOpaque *dovi_parse_unspec62_nalu(const uint8_t *buf, size_t len);
/**
* # Safety
* The pointer to the opaque struct must be valid.
* Avoid using on opaque pointers obtained through `dovi_parse_rpu_bin_file`.
*
* Free the RpuOpaque
*/
void dovi_rpu_free(DoviRpuOpaque *ptr);
/**
* # Safety
* The pointer to the opaque struct must be valid.
*
* Get the last logged error for the RpuOpaque operations.
*
* On invalid parsing, an error is added.
* The user should manually verify if there is an error, as the parsing does not return an error code.
*/
const char *dovi_rpu_get_error(const DoviRpuOpaque *ptr);
/**
* # Safety
* The data pointer should exist, and be allocated by Rust.
*
* Free a Data buffer
*/
void dovi_data_free(const DoviData *data);
/**
* # Safety
* The struct pointer must be valid.
*
* Writes the encoded RPU as a byte buffer.
* If an error occurs in the writing, it is logged to RpuOpaque.error
*/
const DoviData *dovi_write_rpu(DoviRpuOpaque *ptr);
/**
* # Safety
* The struct pointer must be valid.
*
* Writes the encoded RPU, escapes the bytes for HEVC and prepends the buffer with 0x7C01.
* If an error occurs in the writing, it is logged to RpuOpaque.error
*/
const DoviData *dovi_write_unspec62_nalu(DoviRpuOpaque *ptr);
/**
* # Safety
* The struct pointer must be valid.
* The mode must be between 0 and 4.
*
* Converts the RPU to be compatible with a different Dolby Vision profile.
* Possible modes:
* - 0: Don't modify the RPU
* - 1: Converts the RPU to be MEL compatible
* - 2: Converts the RPU to be profile 8.1 compatible. Both luma and chroma mapping curves are set to no-op.
* This mode handles source profiles 5, 7 and 8.
* - 3: Converts to static profile 8.4
* - 4: Converts to profile 8.1 preserving luma and chroma mapping. Old mode 2 behaviour.
*
* If an error occurs, it is logged to RpuOpaque.error.
* Returns 0 if successful, -1 otherwise.
*/
int32_t dovi_convert_rpu_with_mode(DoviRpuOpaque *ptr,
uint8_t mode);
/**
* # Safety
* The pointer to the opaque struct must be valid.
*
* Get the DoVi RPU header struct.
*/
const DoviRpuDataHeader *dovi_rpu_get_header(const DoviRpuOpaque *ptr);
/**
* # Safety
* The pointer to the struct must be valid.
*
* Frees the memory used by the RPU header.
*/
void dovi_rpu_free_header(const DoviRpuDataHeader *ptr);
/**
* # Safety
* The pointer to the opaque struct must be valid.
*
* Get the DoVi RpuDataMapping struct.
*/
const DoviRpuDataMapping *dovi_rpu_get_data_mapping(const DoviRpuOpaque *ptr);
/**
* # Safety
* The pointer to the struct must be valid.
*
* Frees the memory used by the RpuDataMapping.
*/
void dovi_rpu_free_data_mapping(const DoviRpuDataMapping *ptr);
/**
* # Safety
* The pointer to the opaque struct must be valid.
*
* Get the DoVi VdrDmData struct.
*/
const DoviVdrDmData *dovi_rpu_get_vdr_dm_data(const DoviRpuOpaque *ptr);
/**
* # Safety
* The pointer to the struct must be valid.
*
* Frees the memory used by the VdrDmData struct.
*/
void dovi_rpu_free_vdr_dm_data(const DoviVdrDmData *ptr);
/**
* # Safety
* The pointer to the file path must be valid.
*
* Parses an existing RPU binary file.
*
* Returns the heap allocated `DoviRpuList` as a pointer.
* The returned pointer may be null, or the list could be empty if an error occurred.
*/
const DoviRpuOpaqueList *dovi_parse_rpu_bin_file(const char *path);
/**
* # Safety
* The pointer to the struct must be valid.
*
* Frees the memory used by the DoviRpuOpaqueList struct.
*/
void dovi_rpu_list_free(const DoviRpuOpaqueList *ptr);
/**
* # Safety
* The struct pointer must be valid.
*
* Sets the L5 metadata active area offsets.
* If there is no L5 block present, it is created with the offsets.
*/
int32_t dovi_rpu_set_active_area_offsets(DoviRpuOpaque *ptr,
uint16_t left,
uint16_t right,
uint16_t top,
uint16_t bottom);
/**
* # Safety
* The struct pointer must be valid.
*
* Converts the existing reshaping/mapping to become no-op.
*/
int32_t dovi_rpu_remove_mapping(DoviRpuOpaque *ptr);
/**
* # Safety
* The struct pointer must be valid.
*
* Writes the encoded RPU as `itu_t_t35_payload_bytes` for AV1 ITU-T T.35 metadata OBU
* If an error occurs in the writing, it is logged to RpuOpaque.error
*/
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_payload(DoviRpuOpaque *ptr);
/**
* # Safety
* The struct pointer must be valid.
*
* Writes the encoded RPU a complete AV1 `metadata_itut_t35()` OBU
* If an error occurs in the writing, it is logged to RpuOpaque.error
*/
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_complete(DoviRpuOpaque *ptr);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* DOVI_H */
@@ -1,20 +1,27 @@
package com.edde746.plezy
import android.content.Intent
import android.graphics.SurfaceTexture
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import android.util.Rational
import android.view.KeyEvent
import android.view.TextureView
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterShellArgs
import io.flutter.embedding.android.FlutterTextureView
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.android.TransparencyMode
import io.flutter.embedding.engine.FlutterEngine
@@ -27,9 +34,14 @@ import java.io.File
class MainActivity : FlutterActivity() {
private val PIP_CHANNEL = "app.plezy/pip"
private val EXTERNAL_PLAYER_CHANNEL = "app.plezy/external_player"
private val THEME_CHANNEL = "app.plezy/theme"
companion object {
private const val TAG = "MainActivity"
var usingSkia = false
}
private val PIP_CHANNEL = "com.plezy/pip"
private val EXTERNAL_PLAYER_CHANNEL = "com.plezy/external_player"
private val THEME_CHANNEL = "com.plezy/theme"
private var watchNextPlugin: WatchNextPlugin? = null
// Auto PiP state
@@ -37,6 +49,10 @@ class MainActivity : FlutterActivity() {
private var autoPipWidth: Int = 16
private var autoPipHeight: Int = 9
private fun isAndroidTvDevice(): Boolean {
return packageManager.hasSystemFeature("android.software.leanback")
}
override fun onCreate(savedInstanceState: Bundle?) {
// Apply persisted theme color to the window background before anything
// else renders. This prevents a white flash between the native splash
@@ -108,6 +124,31 @@ class MainActivity : FlutterActivity() {
}
}
override fun getFlutterShellArgs(): FlutterShellArgs {
val args = super.getFlutterShellArgs()
usingSkia = shouldDisableImpeller()
if (usingSkia) args.add("--enable-impeller=false")
return args
}
private fun shouldDisableImpeller(): Boolean {
// Android TV devices — weaker GPUs, less Impeller testing
if (packageManager.hasSystemFeature("android.software.leanback")) return true
// Google Tensor SoC (Mali GPU) — Pixel 6+
// SOC_MODEL may return marketing name ("Tensor G2") or internal ID ("GS201")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val soc = Build.SOC_MODEL
if (soc.startsWith("Tensor", ignoreCase = true) ||
soc.startsWith("GS", ignoreCase = true)) return true
}
// NVIDIA Tegra (Shield TV)
if (Build.MANUFACTURER.equals("NVIDIA", ignoreCase = true)) return true
// Huawei/HONOR Kirin SoCs use Mali GPUs
if (Build.MANUFACTURER.equals("Huawei", ignoreCase = true) ||
Build.MANUFACTURER.equals("HONOR", ignoreCase = true)) return true
return false
}
override fun getRenderMode(): RenderMode {
// Use TextureView so Flutter doesn't occupy a SurfaceView layer.
// This allows the libass subtitle SurfaceView to sit between video and Flutter UI.
@@ -119,6 +160,40 @@ class MainActivity : FlutterActivity() {
return TransparencyMode.transparent
}
override fun onFlutterTextureViewCreated(flutterTextureView: FlutterTextureView) {
val original = flutterTextureView.surfaceTextureListener ?: return
val handler = Handler(Looper.getMainLooper())
var pendingResize: Runnable? = null
var lastWidth = 0
var lastHeight = 0
flutterTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, w: Int, h: Int) {
original.onSurfaceTextureAvailable(surface, w, h)
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, w: Int, h: Int) {
if (w == lastWidth && h == lastHeight) return
lastWidth = w; lastHeight = h
pendingResize?.let { handler.removeCallbacks(it) }
pendingResize = Runnable {
if (flutterTextureView.isAvailable) {
original.onSurfaceTextureSizeChanged(surface, w, h)
}
}
handler.postDelayed(pendingResize!!, 100)
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
original.onSurfaceTextureUpdated(surface)
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
pendingResize?.let { handler.removeCallbacks(it) }
pendingResize = null
lastWidth = 0; lastHeight = 0
return original.onSurfaceTextureDestroyed(surface)
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(MpvPlayerPlugin())
@@ -177,6 +252,7 @@ class MainActivity : FlutterActivity() {
// Splash screen theme: persist user's chosen theme for next launch (API 31+)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, THEME_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getRenderer" -> result.success(if (usingSkia) "Skia" else "Impeller")
"setSplashTheme" -> {
val mode = call.argument<String>("mode")
@@ -208,7 +284,7 @@ class MainActivity : FlutterActivity() {
MethodChannel( flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL ).setMethodCallHandler { call, result ->
when (call.method) {
"isSupported" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAndroidTvDevice())
}
"enter" -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@@ -216,6 +292,11 @@ class MainActivity : FlutterActivity() {
return@setMethodCallHandler
}
if (isAndroidTvDevice()) {
result.success(mapOf("success" to false, "errorCode" to "not_supported"))
return@setMethodCallHandler
}
if (!isPipPermissionGranted()) {
result.success(mapOf("success" to false, "errorCode" to "permission_disabled"))
return@setMethodCallHandler
@@ -224,11 +305,7 @@ class MainActivity : FlutterActivity() {
try {
val width = call.argument<Int>("width") ?: 16
val height = call.argument<Int>("height") ?: 9
val clamped = clampAspectRatio(width, height)
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(clamped.first, clamped.second))
.build()
val params = buildPipParams(width, height)
val success = enterPictureInPictureMode(params)
if (success) {
result.success(mapOf("success" to true))
@@ -242,19 +319,23 @@ class MainActivity : FlutterActivity() {
}
}
"setAutoPipReady" -> {
if (isAndroidTvDevice()) {
autoPipReady = false
result.success(true)
return@setMethodCallHandler
}
autoPipReady = call.argument<Boolean>("ready") ?: false
autoPipWidth = call.argument<Int>("width") ?: 16
autoPipHeight = call.argument<Int>("height") ?: 9
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
val clamped = clampAspectRatio(autoPipWidth, autoPipHeight)
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(clamped.first, clamped.second))
.setAutoEnterEnabled(autoPipReady)
.build()
val params = buildPipParams(autoPipWidth, autoPipHeight, autoEnterEnabled = autoPipReady)
setPictureInPictureParams(params)
} catch (_: Exception) {}
} catch (e: Exception) {
Log.w(TAG, "Failed to set auto-PiP params", e)
}
}
result.success(true)
}
@@ -265,18 +346,19 @@ class MainActivity : FlutterActivity() {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
MethodChannel( flutterEngine!!.dartExecutor.binaryMessenger, PIP_CHANNEL ).invokeMethod( "onPipChanged" , isInPictureInPictureMode)
// Notify ExoPlayer plugin to resize video surface for PiP
flutterEngine?.plugins?.get(ExoPlayerPlugin::class.java)?.let { plugin ->
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
flutterEngine?.let { engine ->
MethodChannel(engine.dartExecutor.binaryMessenger, PIP_CHANNEL).invokeMethod("onPipChanged", isInPictureInPictureMode)
engine.plugins.get(ExoPlayerPlugin::class.java)?.let { plugin ->
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
}
}
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// Auto PiP for API 26-30 (API 31+ uses setAutoEnterEnabled)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
if (!isAndroidTvDevice() &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
autoPipReady && isPipPermissionGranted()) {
try {
@@ -284,12 +366,11 @@ class MainActivity : FlutterActivity() {
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
MethodChannel(messenger, PIP_CHANNEL).invokeMethod("onAutoPipEntering", null)
}
val clamped = clampAspectRatio(autoPipWidth, autoPipHeight)
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(clamped.first, clamped.second))
.build()
val params = buildPipParams(autoPipWidth, autoPipHeight)
enterPictureInPictureMode(params)
} catch (_: Exception) {}
} catch (e: Exception) {
Log.w(TAG, "Failed to enter auto-PiP", e)
}
}
}
@@ -302,12 +383,22 @@ class MainActivity : FlutterActivity() {
) == AppOpsManager.MODE_ALLOWED
}
private fun clampAspectRatio(width: Int, height: Int): Pair<Int, Int> {
val ratio = width.toFloat() / height.toFloat()
return when {
ratio < 0.42f -> Pair(5, 12)
ratio > 2.39f -> Pair(12, 5)
else -> Pair(width, height)
private fun buildPipParams(width: Int, height: Int, autoEnterEnabled: Boolean? = null): PictureInPictureParams {
val (w, h) = if (width <= 0 || height <= 0) {
Pair(16, 9)
} else {
val ratio = width.toFloat() / height.toFloat()
when {
ratio < 1f / 2.39f -> Pair(100, 239)
ratio > 2.39f -> Pair(239, 100)
else -> Pair(width, height)
}
}
val builder = PictureInPictureParams.Builder()
.setAspectRatio(Rational(w, h))
if (autoEnterEnabled != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(autoEnterEnabled)
}
return builder.build()
}
}
@@ -0,0 +1,167 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.common.C
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.SeekPoint
import androidx.media3.extractor.TrackOutput
/**
* Extractor wrapper that enables approximate seeking for MKV files without Cues.
*
* When the underlying extractor reports an [SeekMap.Unseekable] seek map (i.e. the MKV
* has no Cues element), this wrapper replaces it with a proportional byte-position
* estimate and scans for the nearest Cluster boundary after seeking.
*/
@androidx.media3.common.util.UnstableApi
class CuelessSeekExtractorWrapper(
private val delegate: Extractor,
) : Extractor {
companion object {
private const val TAG = "CuelessSeek"
// MKV Cluster element ID: 0x1F43B675 (4-byte EBML Class-D ID)
private val CLUSTER_ID = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75)
private const val SCAN_BUFFER_SIZE = 8192
// Max bytes to scan for a Cluster boundary before giving up
private const val MAX_SCAN_BYTES = 1024 * 1024 // 1 MB
}
private var inputLength: Long = C.LENGTH_UNSET.toLong()
private var needsClusterResync = false
private var isApproximateSeeking = false
private var pendingSeekTimeUs: Long = C.TIME_UNSET
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
override fun init(output: ExtractorOutput) {
delegate.init(SeekInterceptingOutput(output))
}
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int {
if (inputLength == C.LENGTH_UNSET.toLong()) {
inputLength = input.length
}
if (needsClusterResync) {
needsClusterResync = false
return scanForCluster(input, seekPosition)
}
return delegate.read(input, seekPosition)
}
override fun seek(position: Long, timeUs: Long) {
if (isApproximateSeeking && position > 0) {
needsClusterResync = true
pendingSeekTimeUs = timeUs
}
delegate.seek(position, timeUs)
}
override fun release() = delegate.release()
/**
* Scan forward from the current input position to find the next MKV Cluster
* element ID (0x1F43B675). Returns [Extractor.RESULT_SEEK] with the Cluster's
* byte position so ExoPlayer repositions the DataSource there.
*/
private fun scanForCluster(input: ExtractorInput, seekPosition: PositionHolder): Int {
val buffer = ByteArray(SCAN_BUFFER_SIZE)
var totalScanned = 0L
// Carry over last 3 bytes across buffer boundaries to detect split IDs
var carry = ByteArray(0)
while (totalScanned < MAX_SCAN_BYTES) {
val toRead = minOf(SCAN_BUFFER_SIZE, (MAX_SCAN_BYTES - totalScanned).toInt())
val bytesRead: Int
try {
bytesRead = input.read(buffer, 0, toRead)
} catch (_: Exception) {
break
}
if (bytesRead == C.RESULT_END_OF_INPUT) break
// Combine carry + new data for scanning
val scanData = if (carry.isNotEmpty()) carry + buffer.copyOf(bytesRead) else buffer.copyOf(bytesRead)
for (i in 0..scanData.size - 4) {
if (scanData[i] == CLUSTER_ID[0] &&
scanData[i + 1] == CLUSTER_ID[1] &&
scanData[i + 2] == CLUSTER_ID[2] &&
scanData[i + 3] == CLUSTER_ID[3]
) {
// Compute the absolute byte position of this Cluster
val clusterPosition = input.position - bytesRead - carry.size + i
Log.d(TAG, "Found Cluster at byte $clusterPosition (scanned ${totalScanned + i} bytes)")
seekPosition.position = clusterPosition
delegate.seek(clusterPosition, pendingSeekTimeUs)
pendingSeekTimeUs = C.TIME_UNSET
return Extractor.RESULT_SEEK
}
}
// Keep last 3 bytes as carry for next iteration
carry = if (scanData.size >= 3) scanData.copyOfRange(scanData.size - 3, scanData.size) else scanData.copyOf()
totalScanned += bytesRead
}
// Failed to find a Cluster — fall back to position 0
Log.w(TAG, "No Cluster found after scanning $totalScanned bytes, resetting to start")
seekPosition.position = 0
delegate.seek(0, 0)
return Extractor.RESULT_SEEK
}
/**
* ExtractorOutput wrapper that intercepts [seekMap] calls to replace
* [SeekMap.Unseekable] with an approximate proportional SeekMap.
*/
private inner class SeekInterceptingOutput(
private val delegate: ExtractorOutput,
) : ExtractorOutput {
override fun track(id: Int, type: Int): TrackOutput = delegate.track(id, type)
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) {
if (seekMap is SeekMap.Unseekable) {
val durationUs = seekMap.durationUs
if (durationUs != C.TIME_UNSET && durationUs > 0) {
Log.i(TAG, "Replacing Unseekable with approximate SeekMap (duration=${durationUs / 1_000_000}s)")
isApproximateSeeking = true
delegate.seekMap(ApproximateSeekMap(durationUs))
return
}
}
// File has real Cues or unknown duration — pass through
isApproximateSeeking = false
delegate.seekMap(seekMap)
}
}
/**
* Approximate SeekMap that estimates byte positions proportionally.
* Used when the MKV has no Cues but has a known duration.
*/
private inner class ApproximateSeekMap(
private val durationUs: Long,
) : SeekMap {
override fun isSeekable(): Boolean = true
override fun getDurationUs(): Long = durationUs
override fun getSeekPoints(timeUs: Long): SeekMap.SeekPoints {
val length = inputLength
if (length == C.LENGTH_UNSET.toLong() || durationUs <= 0) {
return SeekMap.SeekPoints(SeekPoint(0, 0))
}
val clampedTimeUs = timeUs.coerceIn(0, durationUs)
val position = (clampedTimeUs.toDouble() / durationUs * length).toLong().coerceIn(0, length)
return SeekMap.SeekPoints(SeekPoint(clampedTimeUs, position))
}
}
}
@@ -0,0 +1,85 @@
package com.edde746.plezy.exoplayer
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.os.Build
import android.util.Log
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
object DoviBridge {
private const val TAG = "DoviBridge"
private val nativeLoaded: Boolean by lazy {
try {
System.loadLibrary("dovi_bridge")
true
} catch (_: UnsatisfiedLinkError) {
Log.w(TAG, "Native lib not found")
false
}
}
fun isAvailable(): Boolean = nativeLoaded &&
runCatching { nativeIsConversionPathReady() }.getOrDefault(false)
private fun deviceSupportsDvProfile(profile: Int, minApi: Int = 0): Boolean {
try {
if (Build.VERSION.SDK_INT < minApi) return false
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
return codecList.codecInfos.any { info ->
!info.isEncoder && info.supportedTypes.any { type ->
type.equals("video/dolby-vision", ignoreCase = true) &&
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to query DV profile $profile support", e)
return false
}
}
val deviceSupportsDvProfile7: Boolean by lazy {
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr)
.also { Log.i(TAG, "Device DV Profile 7 support: $it") }
}
val deviceSupportsDvProfile8: Boolean by lazy {
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
.also { Log.i(TAG, "Device DV Profile 8 support: $it") }
}
fun getConversionMode(): DvConversionMode = when {
!isAvailable() -> DvConversionMode.DISABLED
deviceSupportsDvProfile7 -> DvConversionMode.DISABLED // try native first; ExoPlayerCore retries with conversion on failure
deviceSupportsDvProfile8 -> DvConversionMode.DV81
else -> DvConversionMode.HEVC_STRIP
}
/** Get the fallback mode when native DV7 decoding fails. */
fun getDv7FallbackMode(): DvConversionMode = when {
deviceSupportsDvProfile8 -> DvConversionMode.DV81
else -> DvConversionMode.HEVC_STRIP
}
fun convertRpuNalu(payload: ByteArray, mode: Int = 2): ByteArray? {
if (!isAvailable() || payload.isEmpty()) return null
return runCatching { nativeConvertDv7RpuToDv81(payload, mode) }
.onFailure { Log.w(TAG, "RPU conversion failed: ${it.message}") }
.getOrNull()
}
fun getVersion(): String? {
if (!nativeLoaded) return null
return runCatching { nativeGetBridgeVersion() }.getOrNull()
}
@JvmStatic
private external fun nativeConvertDv7RpuToDv81(payload: ByteArray, mode: Int): ByteArray?
@JvmStatic
private external fun nativeIsConversionPathReady(): Boolean
@JvmStatic
private external fun nativeGetBridgeVersion(): String
}
@@ -0,0 +1,437 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.TrackOutput
/**
* TrackOutput wrapper that processes DV Profile 7 HEVC samples based on conversion mode:
*
* - DV81: Convert RPU NALs via libdovi to Profile 8.1, present as video/dolby-vision
* with dvhe.08.XX codec string. Preserves dynamic tone mapping metadata.
* - HEVC_STRIP: Strip all DV enhancement layers, present as plain video/hevc.
*
* Two modes of NAL framing (auto-detected):
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
* - Length-prefixed (MP4 path): Mp4Extractor outputs 4-byte big-endian lengths
*
* NAL processing:
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
* - nuh_layer_id > 0: Enhancement layer NAL → strip
* - All retained NALs: normalize nuh_layer_id to 0
*
* All buffers are reused across samples to minimize GC pressure on the hot path.
*/
class DoviConvertingTrackOutput(
private val delegate: TrackOutput,
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
) : TrackOutput {
companion object {
private const val TAG = "DoviConvertTrack"
private const val NAL_TYPE_UNSPEC62 = 62
private const val NAL_TYPE_UNSPEC63 = 63
private const val LIBDOVI_MODE_TO_81 = 2
private const val INITIAL_BUFFER_SIZE = 256 * 1024
private const val READ_CHUNK = 64 * 1024
private val ANNEX_B_START_CODE = byteArrayOf(0, 0, 0, 1)
}
var conversionActive = false
private set
var strippedNalCount = 0L
private set
var convertedRpuCount = 0L
private set
// Reusable buffers — grown as needed, never shrunk
private var sampleBuf = ByteArray(INITIAL_BUFFER_SIZE)
private var sampleLen = 0
private var outputBuf = ByteArray(INITIAL_BUFFER_SIZE)
private var outputLen = 0
private var readBuf = ByteArray(READ_CHUNK)
private val outputParsable = ParsableByteArray()
private var buffering = false
// Sample counter for periodic logging
private var sampleCount = 0L
override fun format(format: Format) {
if (!conversionActive) {
val codecs = format.codecs
if (codecs != null && codecs.startsWith("dvhe.07")) {
conversionActive = true
Log.i(TAG, "DV Profile 7 detected ($codecs), mode=$dvMode")
Log.i(TAG, "Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
"initData=${format.initializationData.size} entries " +
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})")
val newFormat = when (dvMode) {
DvConversionMode.DV81 -> {
// Parse DV level from codec string: "dvhe.07.06" → 6
val level = codecs.split('.').getOrNull(2)?.toIntOrNull() ?: 6
val newCodecs = "dvhe.08.%02d".format(level)
val dvConfigRecord = buildDv81ConfigRecord(level)
Log.i(TAG, "DV81: rewriting to $newCodecs, config=${dvConfigRecord.size}B")
format.buildUpon()
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
.setCodecs(newCodecs)
.setInitializationData(
if (format.initializationData.isNotEmpty())
listOf(format.initializationData[0], dvConfigRecord)
else
listOf(ByteArray(0), dvConfigRecord)
)
.build()
}
else -> {
// HEVC_STRIP: present as plain HEVC
Log.i(TAG, "HEVC_STRIP: rewriting to video/hevc")
format.buildUpon()
.setSampleMimeType(MimeTypes.VIDEO_H265)
.setCodecs(null)
.setInitializationData(
if (format.initializationData.isNotEmpty())
listOf(format.initializationData[0])
else
emptyList()
)
.build()
}
}
Log.i(TAG, "Rewritten format: mime=${newFormat.sampleMimeType}, " +
"codecs=${newFormat.codecs}, initData=${newFormat.initializationData.size} entries")
delegate.format(newFormat)
return
}
}
delegate.format(format)
}
override fun sampleData(
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int
): Int {
if (!conversionActive) {
return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
}
buffering = true
if (readBuf.size < length) readBuf = ByteArray(length)
val bytesRead = input.read(readBuf, 0, length)
if (bytesRead > 0) {
ensureSampleCapacity(sampleLen + bytesRead)
System.arraycopy(readBuf, 0, sampleBuf, sampleLen, bytesRead)
sampleLen += bytesRead
}
return bytesRead
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
if (!conversionActive) {
delegate.sampleData(data, length, sampleDataPart)
return
}
buffering = true
ensureSampleCapacity(sampleLen + length)
data.readBytes(sampleBuf, sampleLen, length)
sampleLen += length
}
override fun sampleMetadata(
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?
) {
if (!conversionActive || !buffering) {
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
return
}
buffering = false
val srcLen = sampleLen
sampleLen = 0
val outLen: Int
val outBuf: ByteArray
val success = try {
processNalUnits(srcLen)
true
} catch (e: Exception) {
Log.e(TAG, "NAL processing failed, passing raw sample", e)
false
}
if (success) {
outLen = outputLen
outBuf = outputBuf
} else {
outLen = srcLen
outBuf = sampleBuf
}
// Skip empty samples (all NALs were DV layers) — don't confuse the decoder
if (outLen == 0) return
outputParsable.reset(outBuf, outLen)
delegate.sampleData(outputParsable, outLen, TrackOutput.SAMPLE_DATA_PART_MAIN)
delegate.sampleMetadata(timeUs, flags, outLen, 0, cryptoData)
}
/**
* Process NAL units in sampleBuf[0..dataLen). Auto-detects format:
* - Annex B (00 00 00 01 / 00 00 01 start codes) — used by MatroskaExtractor
* - Length-prefixed (4-byte big-endian length) — used by Mp4Extractor
*
* Result is written to outputBuf[0..outputLen).
*/
private fun processNalUnits(dataLen: Int) {
outputLen = 0
if (dataLen < 4) {
ensureOutputCapacity(dataLen)
System.arraycopy(sampleBuf, 0, outputBuf, 0, dataLen)
outputLen = dataLen
return
}
// Auto-detect: Annex B starts with 00 00 00 01 or 00 00 01
val isAnnexB = (dataLen >= 4 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() &&
sampleBuf[2] == 0.toByte() && sampleBuf[3] == 1.toByte()) ||
(dataLen >= 3 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() &&
sampleBuf[2] == 1.toByte())
if (sampleCount == 0L) {
Log.d(TAG, "NAL format detected: ${if (isAnnexB) "Annex B" else "length-prefixed"}, " +
"first bytes: ${sampleBuf.take(8).joinToString(" ") { "%02X".format(it) }}")
}
if (isAnnexB) processAnnexBNals(dataLen) else processLengthPrefixedNals(dataLen)
}
/** Process Annex B formatted NAL units (MKV path). Scans inline, no list allocation. */
private fun processAnnexBNals(dataLen: Int) {
ensureOutputCapacity(dataLen)
var kept = 0
var stripped = 0
// Find first start code
var scEnd = -1
var i = 0
while (i < dataLen - 2) {
if (sampleBuf[i] == 0.toByte() && sampleBuf[i + 1] == 0.toByte()) {
if (i + 3 < dataLen && sampleBuf[i + 2] == 0.toByte() && sampleBuf[i + 3] == 1.toByte()) {
scEnd = i + 4
break
} else if (sampleBuf[i + 2] == 1.toByte()) {
scEnd = i + 3
break
}
}
i++
}
if (scEnd < 0) {
// No start codes found — pass through
System.arraycopy(sampleBuf, 0, outputBuf, 0, dataLen)
outputLen = dataLen
sampleCount++
return
}
var nalStart = scEnd
while (nalStart < dataLen) {
// Find next start code to determine end of current NAL
var nalEnd = dataLen
i = nalStart
while (i < dataLen - 2) {
if (sampleBuf[i] == 0.toByte() && sampleBuf[i + 1] == 0.toByte()) {
if (i + 3 < dataLen && sampleBuf[i + 2] == 0.toByte() && sampleBuf[i + 3] == 1.toByte()) {
nalEnd = i
break
} else if (sampleBuf[i + 2] == 1.toByte()) {
nalEnd = i
break
}
}
i++
}
val nalLen = nalEnd - nalStart
if (nalLen > 0) {
val action = processNalInline(nalStart, nalLen)
if (action == NalAction.KEEP) {
ensureOutputCapacity(outputLen + 4 + nalLen)
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
outputLen += 4
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
normalizeLayerId(outputBuf, outputLen)
outputLen += nalLen
kept++
} else if (action == NalAction.CONVERT) {
val converted = DoviBridge.convertRpuNalu(
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81
)
if (converted != null) {
normalizeLayerId(converted, 0)
ensureOutputCapacity(outputLen + 4 + converted.size)
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
outputLen += 4
System.arraycopy(converted, 0, outputBuf, outputLen, converted.size)
outputLen += converted.size
convertedRpuCount++
kept++
} else {
strippedNalCount++
stripped++
}
} else {
strippedNalCount++
stripped++
}
}
// Advance past the next start code
if (nalEnd >= dataLen) break
nalStart = if (nalEnd + 3 < dataLen && sampleBuf[nalEnd + 2] == 0.toByte() && sampleBuf[nalEnd + 3] == 1.toByte()) {
nalEnd + 4
} else {
nalEnd + 3
}
}
sampleCount++
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
Log.d(TAG, "Sample #$sampleCount (AnnexB): ${dataLen}B -> ${outputLen}B, " +
"kept=$kept stripped=$stripped NALs")
}
}
/** Process length-prefixed NAL units (MP4 path). */
private fun processLengthPrefixedNals(dataLen: Int) {
ensureOutputCapacity(dataLen)
var pos = 0
var kept = 0
var stripped = 0
while (pos + 4 <= dataLen) {
val nalLen = ((sampleBuf[pos].toInt() and 0xFF) shl 24) or
((sampleBuf[pos + 1].toInt() and 0xFF) shl 16) or
((sampleBuf[pos + 2].toInt() and 0xFF) shl 8) or
(sampleBuf[pos + 3].toInt() and 0xFF)
if (nalLen <= 0 || pos + 4 + nalLen > dataLen) {
if (sampleCount < 5) {
Log.w(TAG, "Bad NAL length $nalLen at pos $pos (data.size=$dataLen)")
}
break
}
val nalStart = pos + 4
val action = processNalInline(nalStart, nalLen)
if (action == NalAction.KEEP) {
ensureOutputCapacity(outputLen + 4 + nalLen)
writeInt32BE(outputBuf, outputLen, nalLen)
outputLen += 4
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
normalizeLayerId(outputBuf, outputLen)
outputLen += nalLen
kept++
} else if (action == NalAction.CONVERT) {
val converted = DoviBridge.convertRpuNalu(
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81
)
if (converted != null) {
normalizeLayerId(converted, 0)
ensureOutputCapacity(outputLen + 4 + converted.size)
writeInt32BE(outputBuf, outputLen, converted.size)
outputLen += 4
System.arraycopy(converted, 0, outputBuf, outputLen, converted.size)
outputLen += converted.size
convertedRpuCount++
kept++
} else {
strippedNalCount++
stripped++
}
} else {
strippedNalCount++
stripped++
}
pos += 4 + nalLen
}
sampleCount++
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
Log.d(TAG, "Sample #$sampleCount (LenPrefix): ${dataLen}B -> ${outputLen}B, " +
"kept=$kept stripped=$stripped NALs")
}
}
private enum class NalAction { KEEP, STRIP, CONVERT }
/** Classify a NAL at sampleBuf[offset..offset+len) without copying. */
private fun processNalInline(offset: Int, len: Int): NalAction {
if (len < 2) return NalAction.KEEP
val nalType = (sampleBuf[offset].toInt() ushr 1) and 0x3F
val nuhLayerId = ((sampleBuf[offset].toInt() and 1) shl 5) or
((sampleBuf[offset + 1].toInt() ushr 3) and 0x1F)
return when {
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> NalAction.CONVERT
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> NalAction.STRIP
else -> NalAction.KEEP
}
}
private fun normalizeLayerId(data: ByteArray, offset: Int) {
if (data.size - offset >= 2) {
data[offset] = (data[offset].toInt() and 0xFE).toByte()
data[offset + 1] = (data[offset + 1].toInt() and 0x07).toByte()
}
}
private fun writeInt32BE(buf: ByteArray, offset: Int, value: Int) {
buf[offset] = ((value ushr 24) and 0xFF).toByte()
buf[offset + 1] = ((value ushr 16) and 0xFF).toByte()
buf[offset + 2] = ((value ushr 8) and 0xFF).toByte()
buf[offset + 3] = (value and 0xFF).toByte()
}
private fun ensureSampleCapacity(needed: Int) {
if (sampleBuf.size < needed) {
sampleBuf = sampleBuf.copyOf(maxOf(needed, sampleBuf.size * 2))
}
}
private fun ensureOutputCapacity(needed: Int) {
if (outputBuf.size < needed) {
outputBuf = outputBuf.copyOf(maxOf(needed, outputBuf.size * 2))
}
}
/**
* Build a 24-byte DOVIDecoderConfigurationRecord for DV Profile 8.1.
*
* Binary layout (from Dolby Vision spec):
* byte[0]: dv_version_major = 1
* byte[1]: dv_version_minor = 0
* byte[2]: dv_profile (7 bits) | dv_level MSB (1 bit)
* byte[3]: dv_level low 5 bits (5 bits) | rpu_present (1) | el_present (1) | bl_present (1)
* byte[4]: bl_compatibility_id (4 bits) | md_compression (2 bits) | reserved (2 bits)
* byte[5-23]: reserved (zeros)
*/
private fun buildDv81ConfigRecord(level: Int): ByteArray {
val record = ByteArray(24)
record[0] = 0x01 // dv_version_major = 1
record[1] = 0x00 // dv_version_minor = 0
record[2] = ((8 shl 1) or ((level ushr 5) and 0x01)).toByte() // profile=8 | level MSB
record[3] = (((level and 0x1F) shl 3) or 0x05).toByte() // level low 5 | rpu=1 el=0 bl=1
record[4] = (1 shl 4).toByte() // bl_compatibility_id=1 (HDR10)
// bytes 5-23 remain 0 (reserved)
return record
}
}
@@ -0,0 +1,60 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.C
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
/**
* ExtractorOutput wrapper that intercepts video track creation
* to insert DoviConvertingTrackOutput for DV processing.
* Shared by DoviExtractorWrapper for both MP4 and MKV containers.
*/
class DoviExtractorOutputWrapper(
private val delegate: ExtractorOutput,
private val dvMode: DvConversionMode,
private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit,
) : ExtractorOutput {
override fun track(id: Int, type: Int): TrackOutput {
val original = delegate.track(id, type)
if (type == C.TRACK_TYPE_VIDEO) {
val wrapper = DoviConvertingTrackOutput(original, dvMode)
onVideoTrackWrapped(wrapper)
return wrapper
}
return original
}
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
}
/**
* Extractor decorator for Mp4/FragmentedMp4 containers.
* Wraps the video TrackOutput with DoviConvertingTrackOutput to perform
* DV Profile 7 → 8.1 conversion via inline NAL processing.
*/
class DoviExtractorWrapper(
private val delegate: Extractor,
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
) : Extractor {
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
private set
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
override fun init(output: ExtractorOutput) {
delegate.init(DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it })
}
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int =
delegate.read(input, seekPosition)
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
override fun release() = delegate.release()
}
File diff suppressed because it is too large Load Diff
@@ -2,11 +2,13 @@ package com.edde746.plezy.exoplayer
import android.app.Activity
import android.app.ActivityManager
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.edde746.plezy.mpv.MpvPlayerCore
import com.edde746.plezy.mpv.MpvPlayerDelegate
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -15,7 +17,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate, MpvPlayerDelegate {
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate {
companion object {
private const val TAG = "ExoPlayerPlugin"
@@ -33,9 +35,12 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private var activity: Activity? = null
private var activityBinding: ActivityPluginBinding? = null
private val nameToId = mutableMapOf<String, Int>()
private val mainHandler = Handler(Looper.getMainLooper())
private var configuredBufferSizeBytes: Int? = null
private var configuredTunnelingEnabled: Boolean = true
private var sessionGeneration = 0
private var debugLoggingEnabled: Boolean = false
private val pendingMpvProperties = mutableListOf<Pair<String, String>>()
// FlutterPlugin
@@ -64,6 +69,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
override fun onDetachedFromActivity() {
sessionGeneration++
playerCore?.dispose()
playerCore = null
mpvCore?.dispose()
@@ -82,6 +88,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
override fun onDetachedFromActivityForConfigChanges() {
sessionGeneration++
fallbackInProgress = false
activity = null
activityBinding = null
Log.d(TAG, "Detached from activity for config changes")
@@ -116,6 +124,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"selectSubtitleTrack" -> handleSelectSubtitleTrack(call, result)
"addSubtitleTrack" -> handleAddSubtitleTrack(call, result)
"setVisible" -> handleSetVisible(call, result)
"updateFrame" -> handleUpdateFrame(result)
"setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
"requestAudioFocus" -> handleRequestAudioFocus(result)
@@ -132,12 +141,17 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
"observeProperty" -> handleObserveProperty(call, result)
"setMpvProperty" -> handleSetMpvProperty(call, result)
"setLogLevel" -> {
val level = call.argument<String>("level") ?: "warn"
debugLoggingEnabled = (level == "v" || level == "debug" || level == "trace")
playerCore?.debugLoggingEnabled = debugLoggingEnabled
result.success(null)
}
"triggerFallback" -> {
playerCore?.triggerFallback()
result.success(null)
}
else -> result.notImplemented()
}
}
@@ -158,9 +172,17 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
configuredBufferSizeBytes = bufferSizeBytes
configuredTunnelingEnabled = tunnelingEnabled
currentActivity.runOnUiThread {
sessionGeneration++
if (mpvCore != null || fallbackInProgress) {
mpvCore?.dispose()
mpvCore = null
usingMpvFallback = false
fallbackInProgress = false
}
try {
playerCore = ExoPlayerCore(currentActivity).apply {
delegate = this@ExoPlayerPlugin
@@ -185,6 +207,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun handleDispose(result: MethodChannel.Result) {
activity?.runOnUiThread {
sessionGeneration++
playerCore?.dispose()
playerCore = null
mpvCore?.dispose()
@@ -196,18 +219,26 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} ?: result.success(null)
}
@Suppress("UNCHECKED_CAST")
private fun handleOpen(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")
val headers = call.argument<Map<String, String>>("headers")
val startPositionMs = call.argument<Number>("startPositionMs")?.toLong() ?: 0L
val autoPlay = call.argument<Boolean>("autoPlay") ?: true
val isLive = call.argument<Boolean>("isLive") ?: false
val externalSubtitles = call.argument<List<Map<String, String?>>>("externalSubtitles")
if (uri == null) {
result.error("INVALID_ARGS", "Missing 'uri'", null)
return
}
// Only clear pending MPV properties when MPV is the active backend.
// When ExoPlayer is active, keep them for potential ExoPlayer→MPV fallback.
if (usingMpvFallback) {
pendingMpvProperties.clear()
}
activity?.runOnUiThread {
if (usingMpvFallback) {
// MPV: Build loadfile command with options
@@ -223,7 +254,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
} else {
playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive)
playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive, externalSubtitles)
}
result.success(null)
} ?: result.error("NO_ACTIVITY", "Activity not available", null)
@@ -328,9 +359,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
activity?.runOnUiThread {
if (usingMpvFallback) {
// MPV uses numeric track IDs - extract from string format
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
mpvCore?.setProperty("aid", numericId.toString())
// After fallback, track IDs come from mpv's track-list (already 1-indexed)
mpvCore?.setProperty("aid", trackId)
} else {
playerCore?.selectAudioTrack(trackId)
}
@@ -344,12 +374,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// trackId can be null or "no" to disable subtitles
activity?.runOnUiThread {
if (usingMpvFallback) {
if (trackId == null || trackId == "no") {
mpvCore?.setProperty("sid", "no")
} else {
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
mpvCore?.setProperty("sid", numericId.toString())
}
mpvCore?.setProperty("sid", trackId ?: "no")
} else {
playerCore?.selectSubtitleTrack(trackId)
}
@@ -396,6 +421,15 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.success(null)
}
private fun handleUpdateFrame(result: MethodChannel.Result) {
if (usingMpvFallback) {
mpvCore?.updateFrame()
} else {
playerCore?.updateFrame()
}
result.success(null)
}
private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
@@ -460,6 +494,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
val bgColor = call.argument<String>("bgColor") ?: "#000000"
val bgOpacity = call.argument<Number>("bgOpacity")?.toInt() ?: 0
val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
val bold = call.argument<Boolean>("bold") ?: false
val italic = call.argument<Boolean>("italic") ?: false
if (usingMpvFallback) {
// MPV fallback handles styling via setProperty, no-op here
@@ -467,21 +503,48 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
return
}
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition)
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic)
result.success(null)
}
private fun handleSetMpvProperty(call: MethodCall, result: MethodChannel.Result) {
val name = call.argument<String>("name")
val value = call.argument<String>("value")
if (name == null || value == null) {
result.error("INVALID_ARGS", "Missing 'name' or 'value'", null)
return
}
// Apply sync offsets to ExoPlayer when active
if (!usingMpvFallback) {
when (name) {
"audio-delay" -> playerCore?.setAudioDelay(value.toDoubleOrNull() ?: 0.0)
"sub-delay" -> playerCore?.setSubtitleDelay(value.toDoubleOrNull() ?: 0.0)
}
}
if (usingMpvFallback) {
mpvCore?.setProperty(name, value)
} else {
// Store for later application if ExoPlayer falls back to MPV
pendingMpvProperties.add(Pair(name, value))
}
result.success(null)
}
private fun handleGetStats(result: MethodChannel.Result) {
activity?.runOnUiThread {
val stats = if (usingMpvFallback) {
// For MPV fallback, query MPV properties directly
getMpvStats()
} else {
if (usingMpvFallback) {
Thread {
val stats = getMpvStats()
activity?.runOnUiThread { result.success(stats) }
}.start()
} else {
activity?.runOnUiThread {
val coreStats = playerCore?.getStats() ?: emptyMap()
coreStats + mapOf("playerType" to "exoplayer")
}
result.success(stats)
} ?: result.success(mapOf("playerType" to "unknown"))
result.success(coreStats + mapOf("playerType" to "exoplayer"))
} ?: result.success(mapOf("playerType" to "unknown"))
}
}
/**
@@ -500,6 +563,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"video-codec" to mpv.getProperty("video-codec"),
"video-params/w" to mpv.getProperty("video-params/w"),
"video-params/h" to mpv.getProperty("video-params/h"),
"videoWidth" to mpv.getProperty("dwidth"),
"videoHeight" to mpv.getProperty("dheight"),
"container-fps" to mpv.getProperty("container-fps"),
"estimated-vf-fps" to mpv.getProperty("estimated-vf-fps"),
"video-bitrate" to mpv.getProperty("video-bitrate"),
@@ -555,7 +620,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
override fun onPropertyChange(name: String, value: Any?) {
val propId = nameToId[name] ?: return
eventSink?.success(listOf(propId, value))
mainHandler.post { eventSink?.success(listOf(propId, value)) }
}
override fun onEvent(name: String, data: Map<String, Any>?) {
@@ -564,7 +629,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"name" to name
)
data?.let { event["data"] = it }
eventSink?.success(event)
mainHandler.post { eventSink?.success(event) }
}
/**
@@ -572,11 +637,14 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
* or null if the URI is not a content:// scheme or opening fails.
* The returned FD is detached so MPV can own and close it via fdclose://.
*/
private fun openContentFd(uriString: String): Int? {
private fun openContentFd(
uriString: String,
resolver: ContentResolver? = activity?.contentResolver
): Int? {
if (!uriString.startsWith("content://")) return null
return try {
val uri = Uri.parse(uriString)
val pfd = activity?.contentResolver?.openFileDescriptor(uri, "r") ?: return null
val pfd = resolver?.openFileDescriptor(uri, "r") ?: return null
val fd = pfd.detachFd()
Log.d(TAG, "Opened content FD $fd for $uriString")
fd
@@ -615,69 +683,136 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
playerCore = null
mpvCore?.dispose()
mpvCore = null
usingMpvFallback = false // Clear before handoff
// Create and initialize MPV
mpvCore = MpvPlayerCore(currentActivity).apply {
delegate = this@ExoPlayerPlugin
}
mpvCore?.initialize { success ->
if (!success) {
val generation = sessionGeneration
Handler(Looper.getMainLooper()).post {
if (generation != sessionGeneration) {
fallbackInProgress = false
Log.e(TAG, "Failed to initialize MPV fallback")
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
return@initialize
return@post
}
val act = activity
if (act == null) {
fallbackInProgress = false
return@post
}
usingMpvFallback = true
fallbackInProgress = false
// Configure basic MPV properties for Plex playback
mpvCore?.setProperty("hwdec", "mediacodec,mediacodec-copy")
mpvCore?.setProperty("vo", "gpu")
mpvCore?.setProperty("ao", "audiotrack")
// Forward user's buffer config to MPV fallback
configuredBufferSizeBytes?.let { bytes ->
if (bytes > 0) {
mpvCore?.setProperty("demuxer-max-bytes", bytes.toString())
try {
val core = MpvPlayerCore(act).apply {
delegate = this@ExoPlayerPlugin
}
mpvCore = core // publish so dispose/init can reach it
core.initialize { success ->
if (generation != sessionGeneration) {
if (mpvCore === core) {
core.dispose()
mpvCore = null
}
fallbackInProgress = false
return@initialize
}
if (!success) {
if (mpvCore === core) {
core.dispose()
mpvCore = null
}
fallbackInProgress = false
Log.e(TAG, "Failed to initialize MPV fallback")
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
return@initialize
}
usingMpvFallback = true
fallbackInProgress = false
// Snapshot pending properties on main thread before clearing
val pendingProps = pendingMpvProperties.toList()
pendingMpvProperties.clear()
// Compute content FD on main thread (needs contentResolver)
val mpvUri = openContentFd(uri, act.contentResolver)
?.let { "fdclose://$it" } ?: uri
// Buffer size for closure
val bufferSize = configuredBufferSizeBytes
if (mpvCore !== core) {
core.dispose()
fallbackInProgress = false
return@initialize
}
// Configure basic MPV properties for Plex playback
core.setProperty("hwdec", "mediacodec,mediacodec-copy")
core.setProperty("vo", "gpu")
core.setProperty("ao", "audiotrack")
// Forward user's buffer config to MPV fallback
if (bufferSize != null && bufferSize > 0) {
core.setProperty("demuxer-max-bytes", bufferSize.toString())
}
// Apply pending MPV properties from Dart
for ((propName, propValue) in pendingProps) {
core.setProperty(propName, propValue)
}
// Setup property observers
core.observeProperty("time-pos", "double")
core.observeProperty("duration", "double")
core.observeProperty("seekable", "flag")
core.observeProperty("pause", "flag")
core.observeProperty("paused-for-cache", "flag")
core.observeProperty("demuxer-cache-time", "double")
core.observeProperty("eof-reached", "flag")
core.observeProperty("track-list", "string")
core.observeProperty("aid", "string")
core.observeProperty("sid", "string")
core.observeProperty("volume", "double")
core.observeProperty("speed", "double")
// Show the MPV surface (internally posts to UI)
core.setVisible(true)
// Load media at the same position
val startSeconds = positionMs / 1000.0
val options = mutableListOf<String>()
options.add("start=$startSeconds")
headers?.forEach { (key, value) ->
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
core.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
// On GPUs without compute shaders, MPV can't do dynamic peak detection
// and spline tone-mapping produces dim/washed-out results with extreme
// static HDR peak metadata. Use reinhard which handles this better.
Thread {
val peakDetection = core.getProperty("hdr-compute-peak")
if (peakDetection == "no") {
Log.i(TAG, "No compute shaders — overriding tone-mapping to reinhard")
core.setProperty("tone-mapping", "reinhard")
core.setProperty("tone-mapping-param", "0.7")
core.setProperty("tone-mapping-mode", "luma")
}
}.start()
// Request audio focus
core.requestAudioFocus()
// Emit backend-switched event on main thread
activity?.runOnUiThread {
onEvent("backend-switched", null)
}
Log.i(TAG, "Successfully switched to MPV fallback")
}
} catch (e: Exception) {
fallbackInProgress = false
Log.e(TAG, "Failed to switch to MPV fallback", e)
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: ${e.message}"))
}
// Setup property observers
mpvCore?.observeProperty("time-pos", "double")
mpvCore?.observeProperty("duration", "double")
mpvCore?.observeProperty("pause", "flag")
mpvCore?.observeProperty("paused-for-cache", "flag")
mpvCore?.observeProperty("demuxer-cache-time", "double")
mpvCore?.observeProperty("eof-reached", "flag")
mpvCore?.observeProperty("track-list", "string")
mpvCore?.observeProperty("aid", "string")
mpvCore?.observeProperty("sid", "string")
mpvCore?.observeProperty("volume", "double")
mpvCore?.observeProperty("speed", "double")
// Show the MPV surface
mpvCore?.setVisible(true)
// Load media at the same position
val startSeconds = positionMs / 1000.0
val options = mutableListOf<String>()
options.add("start=$startSeconds")
headers?.forEach { (key, value) ->
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
// Request audio focus
mpvCore?.requestAudioFocus()
// Emit backend-switched event so Flutter can show notification
onEvent("backend-switched", null)
Log.i(TAG, "Successfully switched to MPV fallback")
}
} catch (e: Exception) {
fallbackInProgress = false
@@ -0,0 +1,348 @@
package com.edde746.plezy.exoplayer
import android.content.Context
import android.media.AudioDeviceInfo
import android.os.Build
import androidx.annotation.OptIn
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.util.Clock
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.analytics.PlayerId
import androidx.media3.exoplayer.audio.AudioOutput
import androidx.media3.exoplayer.audio.AudioOutputProvider
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.AudioTrackAudioOutputProvider
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider
import androidx.media3.exoplayer.audio.ForwardingAudioSink
import androidx.media3.exoplayer.Renderer
import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.abs
@OptIn(UnstableApi::class)
class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context) {
/** Audio delay in microseconds. Shared with PositionFixAudioSink for live updates. */
val audioDelayUs = AtomicLong(0L)
override fun buildAudioSink(
context: Context,
enableFloatOutput: Boolean,
enableAudioOutputPlaybackParams: Boolean
): AudioSink {
AudioTrackAudioOutputProvider.failOnSpuriousAudioTimestamp = false
val bufferSizeProvider = DefaultAudioTrackBufferSizeProvider.Builder()
.setMinPcmBufferDurationUs(500_000)
.setMaxPcmBufferDurationUs(1_000_000)
.setPcmBufferMultiplicationFactor(4)
.build()
val realProvider = AudioTrackAudioOutputProvider.Builder(context)
.setAudioTrackBufferSizeProvider(bufferSizeProvider)
.build()
// Shared position: RawPositionAudioOutput writes the raw AudioTrack position,
// PositionFixAudioSink reads it to bypass DefaultAudioSink's writtenDuration clamp.
val rawPositionUs = AtomicLong(Long.MIN_VALUE)
val defaultSink = DefaultAudioSink.Builder(context)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioOutputPlaybackParameters(enableAudioOutputPlaybackParams)
.setAudioOutputProvider(RawPositionOutputProvider(realProvider, rawPositionUs))
.build()
return PositionFixAudioSink(defaultSink, rawPositionUs, audioDelayUs)
}
}
/**
* Fixes video stutter with large audio frames (e.g. 120ms Opus).
*
* DefaultAudioSink.getCurrentPositionUs() clamps the playback position to
* writtenDuration via Math.min(positionUs, framesToDurationUs(writtenFrames)).
* With large audio frames, data arrives in bursts and writtenDuration lags behind
* the real AudioTrack playback position between bursts, creating a sawtooth
* position pattern (plateau → jump) that makes the video renderer drop frames.
*
* Fix: when DefaultAudioSink's position falls behind the raw AudioTrack position,
* return startMediaTimeUs + rawPosition instead, giving the video renderer a
* smooth clock to sync against.
*
* Also suppresses Amlogic AudioTrack timestamp discontinuity errors.
*
* The raw AudioTrack position advances at real-time rate regardless of speed
* (AudioTrackPositionTracker reports post-time-stretch output frames). The
* bypass multiplies by speed to convert to media time, matching what
* DefaultAudioSink.applyMediaPositionParameters() does internally. Reference
* points are captured on speed changes for correct mid-playback transitions.
*/
@OptIn(UnstableApi::class)
private class PositionFixAudioSink(
sink: AudioSink,
private val rawPositionUs: AtomicLong,
private val audioDelayUs: AtomicLong
) : ForwardingAudioSink(sink) {
private var startMediaTimeUs = Long.MIN_VALUE
private var suppressedErrorCount = 0
// Speed tracking for position bypass
private var currentSpeed = 1.0f
private var refMediaTimeUs = Long.MIN_VALUE
private var refRawPositionUs = 0L
// Transient counter-offset: after seek/flush, ramp offset from 0 to full
// over recoveryDurationUs to prevent video frame drops.
private var recoveryDurationUs = 0L
override fun handleBuffer(
buffer: ByteBuffer,
presentationTimeUs: Long,
encodedAccessUnitCount: Int
): Boolean {
if (startMediaTimeUs == Long.MIN_VALUE) {
startMediaTimeUs = presentationTimeUs
refMediaTimeUs = presentationTimeUs
refRawPositionUs = 0
val delayUs = audioDelayUs.get()
recoveryDurationUs = if (delayUs == 0L) 0L else maxOf(200_000L, 2L * abs(delayUs))
}
return super.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount)
}
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
// Capture reference point before speed changes
val rawPos = rawPositionUs.get()
if (rawPos > 0 && refMediaTimeUs != Long.MIN_VALUE) {
refMediaTimeUs = refMediaTimeUs + ((rawPos - refRawPositionUs) * currentSpeed).toLong()
refRawPositionUs = rawPos
}
currentSpeed = playbackParameters.speed
super.setPlaybackParameters(playbackParameters)
}
override fun getCurrentPositionUs(sourceEnded: Boolean): Long {
val delegatePos = super.getCurrentPositionUs(sourceEnded)
if (delegatePos == Long.MIN_VALUE || refMediaTimeUs == Long.MIN_VALUE) {
return delegatePos
}
val rawPos = rawPositionUs.get()
val basePos = if (rawPos <= 0) {
delegatePos
} else {
val expectedPos = refMediaTimeUs + ((rawPos - refRawPositionUs) * currentSpeed).toLong()
if (expectedPos > delegatePos + 30_000) expectedPos else delegatePos
}
// Audio delay with transient counter-offset to prevent frame drops after seeks.
// After flush, offset ramps linearly from 0 to full over recoveryDurationUs.
val delayUs = audioDelayUs.get()
if (delayUs == 0L) return basePos
val elapsedUs = basePos - startMediaTimeUs
val netOffsetUs = if (recoveryDurationUs <= 0L || elapsedUs >= recoveryDurationUs) {
delayUs
} else {
(delayUs * elapsedUs) / recoveryDurationUs
}
return basePos + netOffsetUs
}
// --- Suppress timestamp discontinuity errors ---
override fun setListener(listener: AudioSink.Listener) {
super.setListener(@OptIn(UnstableApi::class) object : AudioSink.Listener {
override fun onPositionDiscontinuity() = listener.onPositionDiscontinuity()
override fun onPositionAdvancing(playoutStartSystemTimeUs: Long) = listener.onPositionAdvancing(playoutStartSystemTimeUs)
override fun onUnderrun(bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) = listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)
override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) = listener.onSkipSilenceEnabledChanged(skipSilenceEnabled)
override fun onOffloadBufferEmptying() = listener.onOffloadBufferEmptying()
override fun onOffloadBufferFull() = listener.onOffloadBufferFull()
override fun onAudioCapabilitiesChanged() = listener.onAudioCapabilitiesChanged()
override fun onAudioTrackInitialized(audioTrackConfig: AudioSink.AudioTrackConfig) = listener.onAudioTrackInitialized(audioTrackConfig)
override fun onAudioTrackReleased(audioTrackConfig: AudioSink.AudioTrackConfig) = listener.onAudioTrackReleased(audioTrackConfig)
override fun onSilenceSkipped() = listener.onSilenceSkipped()
override fun onAudioSessionIdChanged(audioSessionId: Int) = listener.onAudioSessionIdChanged(audioSessionId)
override fun onAudioSinkError(audioSinkError: Exception) {
if (isTimestampDiscontinuity(audioSinkError)) {
suppressedErrorCount++
return
}
listener.onAudioSinkError(audioSinkError)
}
})
}
override fun flush() {
startMediaTimeUs = Long.MIN_VALUE
refMediaTimeUs = Long.MIN_VALUE
refRawPositionUs = 0
recoveryDurationUs = 0L
rawPositionUs.set(Long.MIN_VALUE)
super.flush()
}
override fun reset() {
startMediaTimeUs = Long.MIN_VALUE
refMediaTimeUs = Long.MIN_VALUE
refRawPositionUs = 0
recoveryDurationUs = 0L
currentSpeed = 1.0f
rawPositionUs.set(Long.MIN_VALUE)
suppressedErrorCount = 0
super.reset()
}
private fun isTimestampDiscontinuity(e: Exception): Boolean {
val name = e.javaClass.simpleName
val msg = e.message ?: ""
return name == "InvalidAudioTrackTimestampException" ||
name == "UnexpectedDiscontinuityException" ||
msg.contains("timestamp discontinuity", ignoreCase = true)
}
}
/**
* Wraps a text renderer to shift subtitle timing by [delayUs] microseconds.
* Positive delay → subtitles appear later, negative → earlier.
* Only render() needs the offset — the text renderer uses positionUs to decide
* which cues are active; other Renderer methods are timing-independent.
*/
@OptIn(UnstableApi::class)
internal class SubtitleDelayRenderer(
private val delegate: Renderer,
private val delayUs: AtomicLong
) : Renderer by delegate {
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
delegate.render(positionUs - delayUs.get(), elapsedRealtimeUs)
}
}
// --- AudioOutput wrapping: shares raw position with PositionFixAudioSink ---
// Also implements AudioTrack reuse across seeks to avoid expensive teardown/recreation.
// DefaultAudioSink releases the AudioOutput on every flush (seek), which destroys the
// AudioTrack and creates a new one. On Android TV with tunneled playback, this causes
// 7-10s audio dropout while the hardware pipeline reinitializes (Sony Bravia, etc).
// By flushing instead of releasing and caching the output, we skip the teardown cycle.
@OptIn(UnstableApi::class)
private class RawPositionOutputProvider(
private val delegate: AudioOutputProvider,
private val rawPositionUs: AtomicLong
) : AudioOutputProvider {
private var cachedOutput: RawPositionAudioOutput? = null
private var cachedConfig: AudioOutputProvider.OutputConfig? = null
override fun getFormatSupport(config: AudioOutputProvider.FormatConfig) =
delegate.getFormatSupport(config)
override fun getOutputConfig(config: AudioOutputProvider.FormatConfig) =
delegate.getOutputConfig(config)
override fun getAudioOutput(config: AudioOutputProvider.OutputConfig): AudioOutput {
val cached = cachedOutput
if (cached != null && cachedConfig == config) {
cachedOutput = null
return cached
}
cached?.forceRelease()
cachedOutput = null
val realOutput = delegate.getAudioOutput(config)
cachedConfig = config
return RawPositionAudioOutput(realOutput, rawPositionUs, this)
}
fun returnToCache(output: RawPositionAudioOutput) {
val existing = cachedOutput
if (existing != null && existing !== output) {
existing.forceRelease()
}
cachedOutput = output
}
override fun addListener(listener: AudioOutputProvider.Listener) =
delegate.addListener(listener)
override fun removeListener(listener: AudioOutputProvider.Listener) =
delegate.removeListener(listener)
override fun setClock(clock: Clock) = delegate.setClock(clock)
override fun release() {
cachedOutput?.forceRelease()
cachedOutput = null
cachedConfig = null
delegate.release()
}
}
@OptIn(UnstableApi::class)
private class RawPositionAudioOutput(
private val delegate: AudioOutput,
private val rawPositionUs: AtomicLong,
private val provider: RawPositionOutputProvider
) : AudioOutput {
override fun getPositionUs(): Long {
val pos = delegate.getPositionUs()
rawPositionUs.set(pos)
return pos
}
override fun play() = delegate.play()
override fun pause() = delegate.pause()
@Throws(AudioOutput.WriteException::class)
override fun write(buffer: ByteBuffer, size: Int, presentationTimeUs: Long) =
delegate.write(buffer, size, presentationTimeUs)
override fun flush() {
rawPositionUs.set(Long.MIN_VALUE)
delegate.flush()
}
override fun stop() = delegate.stop()
override fun release() {
rawPositionUs.set(Long.MIN_VALUE)
if (Build.VERSION.SDK_INT >= 25) {
delegate.stop()
delegate.flush()
provider.returnToCache(this)
} else {
delegate.release()
}
}
fun forceRelease() {
rawPositionUs.set(Long.MIN_VALUE)
delegate.release()
}
override fun setVolume(volume: Float) = delegate.setVolume(volume)
override fun isOffloadedPlayback() = delegate.isOffloadedPlayback()
override fun getAudioSessionId() = delegate.getAudioSessionId()
override fun getSampleRate() = delegate.getSampleRate()
override fun getBufferSizeInFrames() = delegate.getBufferSizeInFrames()
override fun getPlaybackParameters() = delegate.getPlaybackParameters()
override fun isStalled() = delegate.isStalled()
override fun addListener(listener: AudioOutput.Listener) = delegate.addListener(listener)
override fun removeListener(listener: AudioOutput.Listener) = delegate.removeListener(listener)
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) =
delegate.setPlaybackParameters(playbackParameters)
override fun setOffloadDelayPadding(delayInFrames: Int, paddingInFrames: Int) =
delegate.setOffloadDelayPadding(delayInFrames, paddingInFrames)
override fun setOffloadEndOfStream() = delegate.setOffloadEndOfStream()
override fun setPlayerId(playerId: PlayerId) = delegate.setPlayerId(playerId)
override fun attachAuxEffect(effectId: Int) = delegate.attachAuxEffect(effectId)
override fun setAuxEffectSendLevel(level: Float) = delegate.setAuxEffectSendLevel(level)
override fun setPreferredDevice(preferredDevice: AudioDeviceInfo?) =
delegate.setPreferredDevice(preferredDevice)
}
@@ -0,0 +1,123 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.TrackOutput
import java.util.zip.DataFormatException
import java.util.zip.Inflater
/**
* TrackOutput wrapper that inflates zlib-compressed sample data (MKV ContentCompAlgo 0).
* Each MKV block is independently zlib-compressed; this wrapper decompresses per-sample
* between sampleData() and sampleMetadata() calls.
*
* All buffers are reused across samples to minimize GC pressure on the hot path.
*/
class ZlibInflatingTrackOutput(
private val delegate: TrackOutput,
) : TrackOutput {
companion object {
private const val TAG = "ZlibTrackOutput"
private const val INITIAL_BUFFER_SIZE = 256 * 1024
private const val INFLATE_CHUNK = 64 * 1024
}
var active = false
private val inflater = Inflater()
// Reusable buffers — grown as needed, never shrunk
private var compressedBuf = ByteArray(INITIAL_BUFFER_SIZE)
private var compressedLen = 0
private var inflateBuf = ByteArray(INITIAL_BUFFER_SIZE)
private var readBuf = ByteArray(INFLATE_CHUNK)
private val outputParsable = ParsableByteArray()
private var buffering = false
override fun format(format: Format) = delegate.format(format)
override fun sampleData(
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int
): Int {
if (!active) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
buffering = true
if (readBuf.size < length) readBuf = ByteArray(length)
val bytesRead = input.read(readBuf, 0, length)
if (bytesRead > 0) appendCompressed(readBuf, 0, bytesRead)
return bytesRead
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
if (!active) {
delegate.sampleData(data, length, sampleDataPart)
return
}
buffering = true
ensureCompressedCapacity(compressedLen + length)
data.readBytes(compressedBuf, compressedLen, length)
compressedLen += length
}
override fun sampleMetadata(
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?
) {
if (!active || !buffering) {
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
return
}
buffering = false
val srcLen = compressedLen
compressedLen = 0
val inflatedLen = try {
inflater.reset()
inflater.setInput(compressedBuf, 0, srcLen)
var written = 0
while (!inflater.finished()) {
if (written == inflateBuf.size) growInflateBuf()
val count = inflater.inflate(inflateBuf, written, inflateBuf.size - written)
if (count == 0 && !inflater.finished()) break
written += count
}
written
} catch (e: DataFormatException) {
Log.e(TAG, "Zlib inflate failed (${srcLen}B), passing raw", e)
// Fall back to raw compressed data
ensureInflateCapacity(srcLen)
System.arraycopy(compressedBuf, 0, inflateBuf, 0, srcLen)
srcLen
}
outputParsable.reset(inflateBuf, inflatedLen)
delegate.sampleData(outputParsable, inflatedLen, TrackOutput.SAMPLE_DATA_PART_MAIN)
delegate.sampleMetadata(timeUs, flags, inflatedLen, 0, cryptoData)
}
private fun appendCompressed(src: ByteArray, offset: Int, length: Int) {
ensureCompressedCapacity(compressedLen + length)
System.arraycopy(src, offset, compressedBuf, compressedLen, length)
compressedLen += length
}
private fun ensureCompressedCapacity(needed: Int) {
if (compressedBuf.size < needed) {
compressedBuf = compressedBuf.copyOf(maxOf(needed, compressedBuf.size * 2))
}
}
private fun ensureInflateCapacity(needed: Int) {
if (inflateBuf.size < needed) {
inflateBuf = ByteArray(maxOf(needed, inflateBuf.size * 2))
}
}
private fun growInflateBuf() {
inflateBuf = inflateBuf.copyOf(inflateBuf.size * 2)
}
}
@@ -0,0 +1,116 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.mkv.MatroskaExtractor
import io.github.peerless2012.ass.media.AssHandler
import io.github.peerless2012.ass.media.extractor.AssMatroskaExtractor
import androidx.media3.extractor.text.SubtitleParser
/**
* Extends AssMatroskaExtractor to add support for MKV ContentCompAlgo 0 (zlib).
*
* Media3's MatroskaExtractor only supports ContentCompAlgo 3 (header stripping).
* This subclass intercepts the compression algorithm during track header parsing:
* - Tells the parent it's header stripping (algo 3) to avoid the ParserException
* - Wraps TrackOutputs with ZlibInflatingTrackOutput to decompress per-sample data
* - Skips ContentCompSettings for zlib tracks (not applicable)
*/
class ZlibMatroskaExtractor(
subtitleParserFactory: SubtitleParser.Factory,
assHandler: AssHandler,
) : AssMatroskaExtractor(subtitleParserFactory, assHandler) {
companion object {
private const val TAG = "ZlibMkvExtractor"
// Matroska EBML element IDs
private const val ID_SEGMENT = 0x18538067
private const val ID_TRACK_ENTRY = 0xAE
private const val ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254
private const val ID_CONTENT_COMPRESSION_SETTINGS = 0x4255
private val extractorOutputField by lazy {
MatroskaExtractor::class.java.getDeclaredField("extractorOutput").apply {
isAccessible = true
}
}
}
private var zlibOutput: ZlibExtractorOutputWrapper? = null
private var currentTrackUsesZlib = false
override fun startMasterElement(id: Int, contentPosition: Long, contentSize: Long) {
super.startMasterElement(id, contentPosition, contentSize)
// After super installs AssSubtitleExtractorOutput, wrap it with our zlib layer
if (id == ID_SEGMENT && zlibOutput == null) {
val currentOutput = extractorOutputField.get(this) as ExtractorOutput
val wrapper = ZlibExtractorOutputWrapper(currentOutput)
zlibOutput = wrapper
extractorOutputField.set(this, wrapper)
Log.d(TAG, "Installed zlib ExtractorOutput wrapper")
}
}
override fun integerElement(id: Int, value: Long) {
if (id == ID_CONTENT_COMPRESSION_ALGORITHM && value == 0L) {
currentTrackUsesZlib = true
Log.i(TAG, "Track uses ContentCompAlgo 0 (zlib), will inflate samples")
// Tell parent it's header stripping (algo 3) to avoid ParserException
super.integerElement(id, 3)
return
}
super.integerElement(id, value)
}
override fun binaryElement(id: Int, contentSize: Int, input: ExtractorInput) {
if (id == ID_CONTENT_COMPRESSION_SETTINGS && currentTrackUsesZlib) {
// Skip ContentCompSettings for zlib tracks — parent would store these as
// sampleStrippedBytes and prepend them to every sample, corrupting output.
input.skipFully(contentSize)
return
}
super.binaryElement(id, contentSize, input)
}
override fun endMasterElement(id: Int) {
val wasZlib = currentTrackUsesZlib
super.endMasterElement(id)
if (id == ID_TRACK_ENTRY && wasZlib) {
zlibOutput?.activateLast()
currentTrackUsesZlib = false
Log.i(TAG, "Activated zlib inflation for track")
}
}
/**
* ExtractorOutput wrapper that wraps all TrackOutputs with ZlibInflatingTrackOutput.
* Tracks are created inactive; activateLast() enables inflation for the most recently
* created track (called when we know a track uses zlib compression).
*/
private class ZlibExtractorOutputWrapper(
private val delegate: ExtractorOutput,
) : ExtractorOutput {
private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null
override fun track(id: Int, type: Int): TrackOutput {
val original = delegate.track(id, type)
val wrapper = ZlibInflatingTrackOutput(original)
lastCreatedWrapper = wrapper
return wrapper
}
fun activateLast() {
lastCreatedWrapper?.active = true
}
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
}
}
File diff suppressed because it is too large Load Diff
@@ -11,7 +11,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
EventChannel.StreamHandler, ActivityAware, MpvPlayerDelegate {
EventChannel.StreamHandler, ActivityAware, com.edde746.plezy.shared.PlayerDelegate {
companion object {
private const val TAG = "MpvPlayerPlugin"
@@ -26,6 +26,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private var activity: Activity? = null
private var activityBinding: ActivityPluginBinding? = null
private val nameToId = mutableMapOf<String, Int>()
private var sessionGeneration = 0
// FlutterPlugin
@@ -54,6 +55,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
override fun onDetachedFromActivity() {
++sessionGeneration
playerCore?.dispose()
playerCore = null
activity = null
@@ -96,6 +98,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"observeProperty" -> handleObserveProperty(call, result)
"command" -> handleCommand(call, result)
"setVisible" -> handleSetVisible(call, result)
"updateFrame" -> handleUpdateFrame(result)
"setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
"requestAudioFocus" -> handleRequestAudioFocus(result)
@@ -122,17 +125,28 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
currentActivity.runOnUiThread {
try {
playerCore = MpvPlayerCore(currentActivity).apply {
// Dispose stale core idempotently
playerCore?.dispose()
playerCore = null
val gen = ++sessionGeneration
val core = MpvPlayerCore(currentActivity).apply {
delegate = this@MpvPlayerPlugin
}
playerCore = core
playerCore?.initialize { success ->
core.initialize { success ->
if (gen != sessionGeneration || playerCore !== core) {
Log.d(TAG, "Stale init callback (gen=$gen, current=$sessionGeneration)")
result.success(false)
return@initialize
}
// Start hidden - now safe because setVisible operates on the container,
// not the SurfaceView directly (matching ExoPlayer's approach)
playerCore?.setVisible(false)
core.setVisible(false)
Log.d(TAG, "Initialized: $success")
result.success(success)
} ?: result.success(false)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize: ${e.message}", e)
result.error("INIT_FAILED", e.message, null)
@@ -142,10 +156,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun handleDispose(result: MethodChannel.Result) {
activity?.runOnUiThread {
playerCore?.dispose()
val core = playerCore
++sessionGeneration
playerCore = null
Log.d(TAG, "Disposed")
result.success(null)
core?.dispose {
Log.d(TAG, "Disposed")
result.success(null)
} ?: result.success(null)
} ?: result.success(null)
}
@@ -197,10 +215,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
return
}
// Use async command to prevent ANR - command executes off UI thread
// and result is called back when complete
playerCore?.commandAsync(args.toTypedArray(), result)
?: result.success(null)
playerCore?.command(args.toTypedArray())
result.success(null)
}
private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) {
@@ -215,6 +231,11 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.success(null)
}
private fun handleUpdateFrame(result: MethodChannel.Result) {
playerCore?.updateFrame()
result.success(null)
}
private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
@@ -249,31 +270,35 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
return
}
try {
val uri = Uri.parse(uriString)
val contentResolver = activity?.contentResolver
if (contentResolver == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
val pfd = contentResolver.openFileDescriptor(uri, "r")
if (pfd == null) {
result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null)
return
}
// detachFd() transfers ownership of the FD to the caller (MPV via fdclose://)
val fd = pfd.detachFd()
Log.d(TAG, "Opened content FD $fd for $uriString")
result.success(fd)
} catch (e: Exception) {
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
result.error("OPEN_FAILED", e.message, null)
val contentResolver = activity?.contentResolver
if (contentResolver == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
// Open file descriptor off UI thread to prevent ANR on slow storage
Thread {
try {
val uri = Uri.parse(uriString)
val pfd = contentResolver.openFileDescriptor(uri, "r")
if (pfd == null) {
activity?.runOnUiThread {
result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null)
}
return@Thread
}
val fd = pfd.detachFd()
Log.d(TAG, "Opened content FD $fd for $uriString")
activity?.runOnUiThread { result.success(fd) }
} catch (e: Exception) {
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
activity?.runOnUiThread { result.error("OPEN_FAILED", e.message, null) }
}
}.start()
}
// MpvPlayerDelegate
// PlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {
val propId = nameToId[name] ?: return
@@ -37,16 +37,19 @@ class FrameRateManager(
return
}
if (surface == null) {
Log.d(TAG, "setVideoFrameRate: Surface not available")
return
}
log("fps=$fps, duration=${videoDurationMs}ms, API=${Build.VERSION.SDK_INT}")
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> setFrameRateS(fps, surface, videoDurationMs)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> setFrameRateR(fps, surface)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (surface == null) {
Log.d(TAG, "setVideoFrameRate: Surface not available")
return
}
setFrameRateS(fps, surface, videoDurationMs)
}
// API R's Surface.setFrameRate() only supports seamless switching (no
// CHANGE_FRAME_RATE_ALWAYS), so 60→24Hz won't switch. Fall through to
// preferredDisplayModeId which directly sets the display mode.
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> setFrameRateM(fps)
}
}
@@ -58,6 +61,13 @@ class FrameRateManager(
getDisplayManager().unregisterDisplayListener(it)
displayListener = null
}
// Restore default display mode on API M (preferredDisplayModeId persists)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window?.attributes?.let { attrs ->
attrs.preferredDisplayModeId = 0
activity.window?.attributes = attrs
}
}
}
private fun registerDisplayListener() {
@@ -79,13 +89,6 @@ class FrameRateManager(
getDisplayManager().registerDisplayListener(displayListener, handler)
}
@RequiresApi(Build.VERSION_CODES.R)
private fun setFrameRateR(fps: Float, surface: Surface) {
Log.d(TAG, "setFrameRateR: Setting frame rate to $fps")
surface.setFrameRate(fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
registerDisplayListener()
}
@RequiresApi(Build.VERSION_CODES.S)
private fun setFrameRateS(fps: Float, surface: Surface, videoDurationMs: Long) {
Log.d(TAG, "setFrameRateS: fps=$fps, duration=${videoDurationMs}ms")
@@ -162,7 +165,7 @@ class FrameRateManager(
BigDecimal(mode.refreshRate.toString()).setScale(1, RoundingMode.FLOOR)) {
modeToUse = mode
break
} else if (mode.refreshRate % fps == 0f) {
} else if ((mode.refreshRate % fps).let { it < 0.1f || (fps - it) < 0.1f }) {
modeToUse = mode
break
}
@@ -0,0 +1,6 @@
package com.edde746.plezy.shared
interface PlayerDelegate {
fun onPropertyChange(name: String, value: Any?)
fun onEvent(name: String, data: Map<String, Any>?)
}
@@ -3,11 +3,14 @@ package com.edde746.plezy.watchnext
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.tvprovider.media.tv.TvContractCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.Executors
/**
* Flutter plugin for Android TV Watch Next integration.
@@ -17,7 +20,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
companion object {
private const val TAG = "WatchNextPlugin"
private const val METHOD_CHANNEL = "app.plezy/watch_next"
private const val METHOD_CHANNEL = "com.plezy/watch_next"
private var pendingDeepLink: String? = null
@@ -37,6 +40,8 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var methodChannel: MethodChannel
private var applicationContext: Context? = null
private var watchNextProvider: WatchNextProvider? = null
private val ioExecutor by lazy { Executors.newSingleThreadExecutor() }
private val mainHandler = Handler(Looper.getMainLooper())
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = binding.applicationContext
@@ -49,6 +54,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
methodChannel.setMethodCallHandler(null)
applicationContext = null
watchNextProvider = null
ioExecutor.shutdown()
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@@ -85,7 +91,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
val items = itemsData.mapNotNull { parseWatchNextItem(it) }
result.success(provider.syncWatchNextPrograms(items))
executeOnIo(result) { provider.syncWatchNextPrograms(items) }
}
private fun handleClear(result: MethodChannel.Result) {
@@ -94,7 +100,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.error("NOT_INITIALIZED", "WatchNextProvider not initialized", null)
return
}
result.success(provider.clearAll())
executeOnIo(result) { provider.clearAll() }
}
private fun handleRemove(call: MethodCall, result: MethodChannel.Result) {
@@ -109,7 +115,23 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.error("INVALID_ARGS", "Missing 'contentId' argument", null)
return
}
result.success(provider.removeItem(contentId))
executeOnIo(result) { provider.removeItem(contentId) }
}
private fun executeOnIo(result: MethodChannel.Result, block: () -> Any?) {
try {
ioExecutor.execute {
try {
val value = block()
mainHandler.post { result.success(value) }
} catch (e: Exception) {
Log.e(TAG, "IO operation failed: ${e.message}", e)
mainHandler.post { result.error("IO_ERROR", e.message, null) }
}
}
} catch (e: java.util.concurrent.RejectedExecutionException) {
result.error("SHUTDOWN", "Plugin is shutting down", null)
}
}
private fun handleGetInitialDeepLink(result: MethodChannel.Result) {
+4 -3
View File
@@ -20,9 +20,10 @@ platform :android do
end
debug_info_dir = "./debug-info/#{version_name}+#{version_code}"
git_commit = `git rev-parse --short HEAD`.strip
# Build the Flutter app
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --obfuscate --split-debug-info=#{debug_info_dir}/aab")
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=play-store --obfuscate --split-debug-info=#{debug_info_dir}/aab")
upload_to_play_store(
track: "production",
@@ -31,8 +32,8 @@ platform :android do
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
# Build universal APK for Amazon Appstore (doesn't accept AABs, doesn't auto-target by ABI)
sh("cd #{ENV['PWD']}/.. && flutter build apk --release --target-platform android-arm,android-arm64 --obfuscate --split-debug-info=#{debug_info_dir}/apk")
# Build universal APK for Amazon Appstore (doesn't accept AABs, excludes x86 via env var)
sh("cd #{ENV['PWD']}/.. && AMAZON=1 flutter build apk --release --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=amazon --obfuscate --split-debug-info=#{debug_info_dir}/apk")
upload_to_amazon_appstore(
apk_paths: [
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path fill="url(#linear-gradient)" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><circle cx="24" cy="24" r="24" fill="transparent"/><path fill="currentColor" d="M45.99 14.39 30.17 30.22l-1.46-1.46 16.31-16.32c-.15-.3-.32-.58-.5-.87l-17.2 17.2 2.16 2.16-1.46 1.46-3.62-3.62L43.34 9.82c-.18-.27-.38-.53-.6-.78L23.01 28.78l4.31 4.31-1.46 1.46-14.4-14.4 1.46-1.46 8.62 8.62L41.37 7.46c-.12-.14-.26-.28-.4-.42C36.62 2.69 30.62 0 24 0S11.38 2.69 7.03 7.03C2.69 11.37 0 17.37 0 24c0 13.25 10.75 24 24 24 6.62 0 12.62-2.69 16.97-7.03C45.31 36.64 48 30.64 48 24c0-3.42-.71-6.67-2.01-9.61ZM21.54 25.9l-7.93-7.92 1.46-1.46L23 24.44l-1.46 1.46Zm2.16-2.16-7.93-7.92 1.46-1.46 7.93 7.92-1.46 1.46Zm.3 19.65c-10.7 0-19.39-8.7-19.39-19.39S13.3 4.61 24 4.61c2.91 0 5.72.63 8.33 1.88l-.89 1.88c-2.33-1.11-4.84-1.68-7.43-1.68-9.55 0-17.33 7.77-17.33 17.32s7.78 17.33 17.33 17.33c6.82 0 13.03-4.03 15.82-10.25l1.89.84C38.6 38.9 31.65 43.41 24.01 43.41Z"/></svg>

After

Width:  |  Height:  |  Size: 921 B

+28 -98
View File
@@ -3,94 +3,50 @@ PODS:
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- in_app_review (2.0.0):
- Flutter
- MLImage (1.0.0-beta6)
- MLKitBarcodeScanning (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitCommon (12.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta6)
- MLKitCommon (~> 12.0)
- mobile_scanner (6.0.2):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- os_media_controls (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Sentry/HybridSDK (8.58.0)
- sentry_flutter (9.16.0):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- Sentry/HybridSDK (= 8.58.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.51.1):
- sqlite3/common (= 3.51.1)
- sqlite3/common (3.51.1)
- sqlite3/dbstatvtab (3.51.1):
- sqlite3 (3.52.0):
- sqlite3/common (= 3.52.0)
- sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.52.0):
- sqlite3/common
- sqlite3/fts5 (3.51.1):
- sqlite3/fts5 (3.52.0):
- sqlite3/common
- sqlite3/math (3.51.1):
- sqlite3/math (3.52.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.51.1):
- sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common
- sqlite3/rtree (3.51.1):
- sqlite3/rtree (3.52.0):
- sqlite3/common
- sqlite3/session (3.51.1):
- sqlite3/session (3.52.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.51.1)
- sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -103,41 +59,28 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- workmanager_apple (0.0.1):
- Flutter
DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- os_media_controls (from `.symlinks/plugins/os_media_controls/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- universal_gamepad (from `.symlinks/plugins/universal_gamepad/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
- Sentry
- sqlite3
EXTERNAL SOURCES:
@@ -145,6 +88,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/background_downloader/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
@@ -153,14 +98,12 @@ EXTERNAL SOURCES:
:path: Flutter
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
os_media_controls:
:path: ".symlinks/plugins/os_media_controls/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
@@ -173,39 +116,26 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
workmanager_apple:
:path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
universal_gamepad: e10172778a8a399cce234494968f38724974919e
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: 16bb7f67e16d8aaa2bfb97666803fbfb291b6559
+80 -86
View File
@@ -10,15 +10,18 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C12D873DE8572579684A82C /* Pods_Runner.framework */; };
69E1B4BB161DC9D84D1DFC62 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB346A1D0705AB171F80B11B /* Pods_Runner.framework */; };
6A48DFB02EA70C7100C1F7CD /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6A48DFAF2EA70C7100C1F7CD /* plezy.icon */; };
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */ = {isa = PBXBuildFile; productRef = 6A8A46202EDB320D0057B88C /* MPVKit-GPL */; };
6A8A46212EDB320D0057B88C /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6A8A46202EDB320D0057B88C /* MPVKit */; };
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */; };
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
748A259343D7FF8DFD2C3910 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */; };
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F969587D0E464D999910F4 /* MpvPipController.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
F37BB572E0F0606E20596E43 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */; };
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */; };
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -47,29 +50,32 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1E1EC7646F3085BF9127AB2A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
2C12D873DE8572579684A82C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
83B9EB35C25EEF40090F6A88 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
7F4B79C5218D46EDB4E433B0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
92F969587D0E464D999910F4 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A8738BEC00467F05948734DD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = SOURCE_ROOT; };
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -77,7 +83,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F37BB572E0F0606E20596E43 /* Pods_RunnerTests.framework in Frameworks */,
748A259343D7FF8DFD2C3910 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -85,8 +91,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */,
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */,
6A8A46212EDB320D0057B88C /* MPVKit in Frameworks */,
69E1B4BB161DC9D84D1DFC62 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -101,11 +107,11 @@
path = RunnerTests;
sourceTree = "<group>";
};
67716449E04A72AA71A90156 /* Frameworks */ = {
3DBA2EB0C0219E797C60DE48 /* Frameworks */ = {
isa = PBXGroup;
children = (
2C12D873DE8572579684A82C /* Pods_Runner.framework */,
3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */,
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */,
75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -113,8 +119,11 @@
6A8A46242EDB370C0057B88C /* MpvPlayer */ = {
isa = PBXGroup;
children = (
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */,
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */,
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
92F969587D0E464D999910F4 /* MpvPipController.swift */,
);
path = MpvPlayer;
sourceTree = "<group>";
@@ -122,12 +131,12 @@
875C8AEFBBA3759A1849D674 /* Pods */ = {
isa = PBXGroup;
children = (
A8738BEC00467F05948734DD /* Pods-Runner.debug.xcconfig */,
83B9EB35C25EEF40090F6A88 /* Pods-Runner.release.xcconfig */,
1E1EC7646F3085BF9127AB2A /* Pods-Runner.profile.xcconfig */,
5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */,
D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */,
87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */,
7F4B79C5218D46EDB4E433B0 /* Pods-Runner.debug.xcconfig */,
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */,
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */,
28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */,
3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */,
792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -151,8 +160,8 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
875C8AEFBBA3759A1849D674 /* Pods */,
67716449E04A72AA71A90156 /* Frameworks */,
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */,
3DBA2EB0C0219E797C60DE48 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -187,7 +196,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
684A4A968F313A7FDAEBF3B8 /* [CP] Check Pods Manifest.lock */,
87A3350E66D519725E5CD790 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
582EBCC1C90BEBEC365D972D /* Frameworks */,
@@ -206,15 +215,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
01D58DD9E289D97FD8DBE66E /* [CP] Check Pods Manifest.lock */,
6838C6976E568CE05DBDE228 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
2E10BAE564A4AFFCFF5B4EF0 /* [CP] Embed Pods Frameworks */,
BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */,
898098BAEC57934A6822754B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -289,7 +297,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
01D58DD9E289D97FD8DBE66E /* [CP] Check Pods Manifest.lock */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
6838C6976E568CE05DBDE228 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -311,40 +335,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
2E10BAE564A4AFFCFF5B4EF0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
684A4A968F313A7FDAEBF3B8 /* [CP] Check Pods Manifest.lock */ = {
87A3350E66D519725E5CD790 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -366,6 +357,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
898098BAEC57934A6822754B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -381,23 +389,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -413,8 +404,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */,
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */,
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
@@ -530,7 +524,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -548,7 +542,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -564,7 +558,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = 792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -783,16 +777,16 @@
repositoryURL = "https://github.com/edde746/MPVKit";
requirement = {
kind = revision;
revision = 2e887368b44ce1dc9e1649e7757ec62c2564e792;
revision = b2179eec26c6ceda339e6c6b512e457341768761;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
6A8A46202EDB320D0057B88C /* MPVKit-GPL */ = {
6A8A46202EDB320D0057B88C /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = 6A8A461F2EDB320D0057B88C /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = "MPVKit-GPL";
productName = MPVKit;
};
/* End XCSwiftPackageProductDependency section */
};
@@ -6,7 +6,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "2e887368b44ce1dc9e1649e7757ec62c2564e792"
"revision" : "b2179eec26c6ceda339e6c6b512e457341768761"
}
}
],
@@ -6,7 +6,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "2e887368b44ce1dc9e1649e7757ec62c2564e792"
"revision" : "b2179eec26c6ceda339e6c6b512e457341768761"
}
}
],
+10 -8
View File
@@ -3,18 +3,11 @@ import UIKit
import AVFoundation
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Register MPV player plugin
if let registrar = self.registrar(forPlugin: "MpvPlayerPlugin") {
MpvPlayerPlugin.register(with: registrar)
}
// Configure audio session for media playback
do {
let session = AVAudioSession.sharedInstance()
@@ -28,4 +21,13 @@ import AVFoundation
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
// Register MPV player plugin
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "MpvPlayerPlugin") {
MpvPlayerPlugin.register(with: registrar)
}
}
}
+22
View File
@@ -51,6 +51,7 @@
<string>Main</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
</array>
<key>UISupportedInterfaceOrientations</key>
@@ -66,5 +67,26 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
+369
View File
@@ -0,0 +1,369 @@
import AVKit
import UIKit
/// Delegate to notify the plugin of PiP lifecycle events
protocol MpvPipDelegate: AnyObject {
/// Called when PiP is about to start (system or app-initiated)
func pipWillStart()
func pipDidStart()
/// Called when PiP stops. `restored` is true if the user pressed maximize (restore UI).
func pipDidStop(restored: Bool)
func pipDidFailToStart(error: Error?)
/// Forward play/pause commands from PiP overlay to mpv
func pipSetPlaying(_ playing: Bool)
/// Forward skip forward/backward commands from PiP overlay to mpv
func pipSkip(byInterval seconds: Double)
/// Query whether mpv is currently playing
var isPipPlaying: Bool { get }
/// Get total duration in seconds
var pipDuration: Double { get }
}
/// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer.
/// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op.
class MpvPipController: NSObject {
// MARK: - Properties
private var pipController: AVPictureInPictureController?
private let sampleBufferLayer = AVSampleBufferDisplayLayer()
private var containerView: UIView?
weak var delegate: MpvPipDelegate?
/// Pointer to the sample buffer layer for passing to mpv as `wid`
var layerPointer: UnsafeMutableRawPointer {
Unmanaged.passUnretained(sampleBufferLayer).toOpaque()
}
// MARK: - Initialization
override init() {
super.init()
setup()
}
private func setup() {
guard #available(iOS 15.0, *) else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("[MpvPipController] Failed to configure audio session: \(error)")
}
// The sample buffer layer must be in a visible view hierarchy for
// isPictureInPicturePossible to become true.
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
let view = UIView(frame: window.bounds)
view.clipsToBounds = true
view.isUserInteractionEnabled = false
sampleBufferLayer.frame = view.bounds
view.layer.addSublayer(sampleBufferLayer)
window.addSubview(view)
containerView = view
}
createPipController()
}
/// Helper that conforms to the iOS 15+ delegate protocols
private var delegateHelper: AnyObject?
private func createPipController() {
guard #available(iOS 15.0, *) else { return }
let helper = PipDelegateHelper(controller: self)
let contentSource = AVPictureInPictureController.ContentSource(
sampleBufferDisplayLayer: sampleBufferLayer,
playbackDelegate: helper
)
self.delegateHelper = helper
pipController = AVPictureInPictureController(contentSource: contentSource)
pipController?.delegate = helper
if #available(iOS 14.2, *) {
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
}
}
/// Enable/disable system auto-PiP (starts PiP automatically on background transition)
func setAutoStart(_ enabled: Bool) {
guard #available(iOS 14.2, *) else { return }
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
}
/// Push a black frame to the sample buffer layer so PiP has content
/// to display immediately (before vo_pip decodes the first real frame).
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {
var pixelBuffer: CVPixelBuffer?
let attrs: [String: Any] = [
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
]
let status = CVPixelBufferCreate(
kCFAllocatorDefault, Int(width), Int(height),
kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer
)
guard status == kCVReturnSuccess, let pb = pixelBuffer else { return }
// Fill with black
CVPixelBufferLockBaseAddress(pb, [])
if let base = CVPixelBufferGetBaseAddress(pb) {
memset(base, 0, CVPixelBufferGetDataSize(pb))
}
CVPixelBufferUnlockBaseAddress(pb, [])
// Use current timebase time for PTS (if available) so the frame isn't stale
let pts: CMTime
if let tb = sampleBufferLayer.controlTimebase {
pts = CMTimebaseGetTime(tb)
} else {
pts = CMTime(value: 0, timescale: 30)
}
var timing = CMSampleTimingInfo(
duration: CMTime(value: 1, timescale: 30),
presentationTimeStamp: pts,
decodeTimeStamp: .invalid
)
// Create format description and sample buffer
var formatDesc: CMVideoFormatDescription?
CMVideoFormatDescriptionCreateForImageBuffer(
allocator: kCFAllocatorDefault,
imageBuffer: pb,
formatDescriptionOut: &formatDesc
)
guard let fmt = formatDesc else { return }
var sampleBuffer: CMSampleBuffer?
CMSampleBufferCreateReadyWithImageBuffer(
allocator: kCFAllocatorDefault,
imageBuffer: pb,
formatDescription: fmt,
sampleTiming: &timing,
sampleBufferOut: &sampleBuffer
)
guard let sb = sampleBuffer else { return }
// Set DisplayImmediately so it shows regardless of timebase timing
if let attachments = CMSampleBufferGetSampleAttachmentsArray(sb, createIfNecessary: true) as? [NSMutableDictionary],
let dict = attachments.first {
dict[kCMSampleAttachmentKey_DisplayImmediately] = true
}
sampleBufferLayer.enqueue(sb)
}
/// Sync the layer's controlTimebase with the actual playback position.
/// This makes the PiP progress bar show the correct time.
func syncTimebase(currentTime: Double, isPlaying: Bool) {
guard let timebase = sampleBufferLayer.controlTimebase else { return }
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
CMTimebaseSetTime(timebase, time: cmTime)
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
}
/// Ensure the layer has a timebase and blank frame so the system considers
/// PiP possible (required for canStartPictureInPictureAutomaticallyFromInline).
func warmLayer(currentTime: Double, isPlaying: Bool) {
if sampleBufferLayer.controlTimebase == nil {
var timebase: CMTimebase?
CMTimebaseCreateWithSourceClock(
allocator: kCFAllocatorDefault,
sourceClock: CMClockGetHostTimeClock(),
timebaseOut: &timebase
)
if let tb = timebase {
sampleBufferLayer.controlTimebase = tb
}
}
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
pushBlankFrame()
}
// MARK: - Public API
static var isSupported: Bool {
guard #available(iOS 15.0, *) else { return false }
return AVPictureInPictureController.isPictureInPictureSupported()
}
/// Start PiP. When `waitForFrame` is false (auto-PiP), skips the frame
/// readiness check since the scene is about to deactivate.
func startPip(waitForFrame: Bool = true, completion: @escaping (Bool) -> Void) {
guard let pipController = pipController else {
completion(false)
return
}
var attempts = 0
func tryStart() {
let possible = pipController.isPictureInPicturePossible
let hasTimebase = sampleBufferLayer.controlTimebase != nil
let hasFrame: Bool
if !waitForFrame {
hasFrame = true // Skip frame check for auto-PiP
} else if #available(iOS 17.4, *) {
hasFrame = sampleBufferLayer.isReadyForDisplay
} else {
hasFrame = true
}
if possible && hasTimebase && hasFrame {
print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP")
pipController.startPictureInPicture()
completion(true)
} else if attempts < 40 {
attempts += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() }
} else {
print("[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))")
completion(false)
}
}
tryStart()
}
func stopPip() {
pipController?.stopPictureInPicture()
}
/// Invalidate the playback state so PiP updates its UI (play/pause button)
func invalidatePlaybackState() {
pipController?.invalidatePlaybackState()
}
/// Fully tear down PiP removes the container view from the window and
/// destroys the AVPictureInPictureController so the system can no longer
/// trigger auto-PiP after the player is disposed.
func teardown() {
pipController?.stopPictureInPicture()
if #available(iOS 14.2, *) {
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
}
pipController = nil
delegateHelper = nil
sampleBufferLayer.flushAndRemoveImage()
sampleBufferLayer.controlTimebase = nil
sampleBufferLayer.removeFromSuperlayer()
containerView?.removeFromSuperview()
containerView = nil
}
/// Flush enqueued sample buffers from the layer to free video frame memory
func flushLayer() {
sampleBufferLayer.flushAndRemoveImage()
}
}
// MARK: - PiP Delegate Helper (iOS 15+)
/// Separate class conforming to AVPictureInPictureControllerDelegate and
/// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+
/// availability for the ContentSource-based delegate methods.
@available(iOS 15.0, *)
private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
AVPictureInPictureSampleBufferPlaybackDelegate
{
weak var controller: MpvPipController?
private var isRestoring = false
init(controller: MpvPipController) {
self.controller = controller
super.init()
}
// MARK: - AVPictureInPictureControllerDelegate
func pictureInPictureControllerWillStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
print("[MpvPipController] PiP will start")
controller?.delegate?.pipWillStart()
}
func pictureInPictureControllerDidStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
print("[MpvPipController] PiP did start")
controller?.delegate?.pipDidStart()
}
func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
let restored = isRestoring
isRestoring = false
print("[MpvPipController] PiP did stop (restored: \(restored))")
controller?.delegate?.pipDidStop(restored: restored)
}
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
failedToStartPictureInPictureWithError error: Error
) {
print("[MpvPipController] PiP failed to start: \(error)")
controller?.delegate?.pipDidFailToStart(error: error)
}
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
print("[MpvPipController] PiP restore user interface")
isRestoring = true
completionHandler(true)
}
func pictureInPictureControllerWillStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
print("[MpvPipController] PiP will stop")
}
// MARK: - AVPictureInPictureSampleBufferPlaybackDelegate
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
setPlaying playing: Bool
) {
print("[MpvPipController] PiP setPlaying: \(playing)")
controller?.delegate?.pipSetPlaying(playing)
}
func pictureInPictureControllerTimeRangeForPlayback(
_ pictureInPictureController: AVPictureInPictureController
) -> CMTimeRange {
let duration = controller?.delegate?.pipDuration ?? 0
if duration > 0 {
return CMTimeRange(
start: .zero,
duration: CMTime(seconds: duration, preferredTimescale: 1000)
)
}
return CMTimeRange(start: .zero, duration: CMTime(seconds: 1, preferredTimescale: 1))
}
func pictureInPictureControllerIsPlaybackPaused(
_ pictureInPictureController: AVPictureInPictureController
) -> Bool {
return !(controller?.delegate?.isPipPlaying ?? false)
}
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
didTransitionToRenderSize newRenderSize: CMVideoDimensions
) {}
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
skipByInterval skipInterval: CMTime,
completion completionHandler: @escaping () -> Void
) {
let seconds = CMTimeGetSeconds(skipInterval)
print("[MpvPipController] PiP skip by \(seconds)s")
controller?.delegate?.pipSkip(byInterval: seconds)
completionHandler()
}
}
+111 -517
View File
@@ -1,66 +1,13 @@
import Libmpv
import UIKit
/// Protocol for receiving player events
protocol MpvPlayerDelegate: AnyObject {
func onPropertyChange(name: String, value: Any?)
func onEvent(name: String, data: [String: Any]?)
}
/// Core MPV player using Metal rendering for iOS.
class MpvPlayerCore: MpvPlayerCoreBase {
// Workaround for MoltenVK problems that cause flicker
// https://github.com/mpv-player/mpv/pull/13651
private class MetalLayer: CAMetalLayer {
override var drawableSize: CGSize {
get { return super.drawableSize }
set {
if Int(newValue.width) > 1 && Int(newValue.height) > 1 {
super.drawableSize = newValue
}
}
}
// Fix for target-colorspace-hint - needs main thread for EDR
@available(iOS 16.0, *)
override var wantsExtendedDynamicRangeContent: Bool {
get { return super.wantsExtendedDynamicRangeContent }
set {
if Thread.isMainThread {
super.wantsExtendedDynamicRangeContent = newValue
} else {
DispatchQueue.main.sync {
super.wantsExtendedDynamicRangeContent = newValue
}
}
}
}
}
/// Core MPV player using Metal rendering for iOS
class MpvPlayerCore: NSObject {
// MARK: - Properties
private var metalLayer: MetalLayer?
private var containerView: UIView? // Container for proper EDR activation
private var mpv: OpaquePointer?
private var containerView: UIView?
private weak var window: UIWindow?
private lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated)
weak var delegate: MpvPlayerDelegate?
private(set) var isInitialized = false
private var isDisposing = false // Flag to prevent race conditions during disposal
// HDR settings
private var hdrEnabled = true // User preference for HDR
private var lastSigPeak: Double = 0.0 // Last known sig-peak for re-evaluation
// Async command tracking to prevent UI blocking
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:]
private var pendingCommandsLock = NSLock()
private var nextRequestId: UInt64 = 1
// MARK: - Initialization
var isPipStarting = false
func initialize(in window: UIWindow) -> Bool {
guard !isInitialized else {
@@ -70,14 +17,11 @@ class MpvPlayerCore: NSObject {
self.window = window
// Create container view for proper EDR activation
// EDR requires the CAMetalLayer to be in a UIView hierarchy, not just window.layer
let container = UIView(frame: window.bounds)
container.backgroundColor = .clear
container.isUserInteractionEnabled = false
// Create Metal layer for video rendering
let layer = MetalLayer()
let layer = MpvMetalLayer()
layer.frame = container.bounds
layer.contentsScale = UIScreen.main.nativeScale
layer.framebufferOnly = true
@@ -87,13 +31,8 @@ class MpvPlayerCore: NSObject {
containerView = container
metalLayer = layer
// Add container view to window (behind Flutter's root view controller)
window.insertSubview(container, at: 0)
print(
"[MpvPlayerCore] Metal layer added to window via container view, frame: \(layer.frame)")
// Initialize MPV with this Metal layer
guard setupMpv() else {
print("[MpvPlayerCore] Failed to setup MPV")
layer.removeFromSuperlayer()
@@ -103,7 +42,6 @@ class MpvPlayerCore: NSObject {
return false
}
// Setup background/foreground notifications
setupNotifications()
isInitialized = true
@@ -111,61 +49,111 @@ class MpvPlayerCore: NSObject {
return true
}
private func setupMpv() -> Bool {
guard let metalLayer = metalLayer else { return false }
func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool {
guard let mpv else { return false }
mpv = mpv_create()
guard mpv != nil else {
print("[MpvPlayerCore] Failed to create MPV context")
return false
}
print("[MpvPlayerCore] Switching to pip VO for PiP")
// Logging
#if DEBUG
checkError(mpv_request_log_messages(mpv, "info"))
#else
checkError(mpv_request_log_messages(mpv, "warn"))
#endif
metalLayer?.removeFromSuperlayer()
// Set the Metal layer as the render target (must use local var for &)
var layer = metalLayer
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
mpv_set_property_string(mpv, "vid", "no")
// Video output settings for Metal/Vulkan
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk"))
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
var pointer = Int64(Int(bitPattern: layerPtr))
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &pointer)
// Initialize MPV
let initResult = mpv_initialize(mpv)
if initResult < 0 {
print(
"[MpvPlayerCore] mpv_initialize failed: \(String(cString: mpv_error_string(initResult)))"
)
mpv_terminate_destroy(mpv)
mpv = nil
return false
}
mpv_set_property_string(mpv, "vo", "pip")
mpv_set_property_string(mpv, "vid", "auto")
// Set up wakeup callback for event handling
mpv_set_wakeup_callback(
mpv,
{ ctx in
guard let ctx = ctx else { return } // Safe guard instead of force unwrap
let core = Unmanaged<MpvPlayerCore>.fromOpaque(ctx).takeUnretainedValue()
core.readEvents()
}, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
// Observe video-params/sig-peak for HDR detection
mpv_observe_property(mpv, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE)
print("[MpvPlayerCore] MPV initialized successfully")
print("[MpvPlayerCore] Switched to pip VO successfully")
return true
}
// MARK: - Background/Foreground Handling
func switchToGpuNextVO() -> Bool {
guard let mpv, let metalLayer else { return false }
print("[MpvPlayerCore] Switching back to gpu-next VO")
mpv_set_property_string(mpv, "vid", "no")
var layer = metalLayer
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &layer)
applyGpuNextOptions()
mpv_set_property_string(mpv, "vid", "auto")
if metalLayer.superlayer == nil, let containerView {
containerView.layer.addSublayer(metalLayer)
}
print("[MpvPlayerCore] Switched back to gpu-next VO successfully")
return true
}
func setVisible(_ visible: Bool) {
guard let containerView else { return }
if visible {
containerView.removeFromSuperview()
window?.insertSubview(containerView, at: 0)
}
containerView.isHidden = !visible
}
func updateFrame(_ frame: CGRect? = nil) {
guard let metalLayer, let containerView else { return }
if let frame {
containerView.frame = frame
metalLayer.frame = containerView.bounds
} else if let window {
containerView.frame = window.bounds
metalLayer.frame = containerView.bounds
}
let scale = UIScreen.main.nativeScale
metalLayer.drawableSize = CGSize(
width: metalLayer.frame.width * scale,
height: metalLayer.frame.height * scale
)
}
/// Nudge mpv to present the current paused frame after switching back from PiP.
func forceDraw() {
command(["seek", "0", "relative+exact"])
}
override func updateEDRMode(sigPeak: Double) {
guard let metalLayer else { return }
var edrHeadroom: CGFloat = 1.0
if #available(iOS 16.0, *) {
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
metalLayer.wantsExtendedDynamicRangeContent =
hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
}
let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
print(
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))"
)
}
func dispose() {
NotificationCenter.default.removeObserver(self)
disposeSharedState(destroySynchronously: false)
metalLayer?.removeFromSuperlayer()
metalLayer = nil
containerView?.removeFromSuperview()
containerView = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
private func setupNotifications() {
NotificationCenter.default.addObserver(
@@ -183,7 +171,11 @@ class MpvPlayerCore: NSObject {
}
@objc private func enterBackground() {
// Disable video output to fix black screen when returning from background
if isPipActive || isPipStarting {
print("[MpvPlayerCore] Entering background - PiP active/starting, keeping video")
return
}
print("[MpvPlayerCore] Entering background - disabling video")
if mpv != nil {
mpv_set_option_string(mpv, "vid", "no")
@@ -191,412 +183,14 @@ class MpvPlayerCore: NSObject {
}
@objc private func enterForeground() {
// Re-enable video output
if isPipActive {
print("[MpvPlayerCore] Entering foreground - PiP active, skipping vid restore")
return
}
print("[MpvPlayerCore] Entering foreground - enabling video")
if mpv != nil {
mpv_set_option_string(mpv, "vid", "auto")
}
}
// MARK: - MPV Properties and Commands
func setProperty(_ name: String, value: String) {
guard mpv != nil else { return }
// Handle custom HDR toggle property
if name == "hdr-enabled" {
let enabled = value == "yes" || value == "true" || value == "1"
setHDREnabled(enabled)
return
}
mpv_set_property_string(mpv, name, value)
}
/// Enable or disable HDR mode
func setHDREnabled(_ enabled: Bool) {
hdrEnabled = enabled
print("[MpvPlayerCore] HDR enabled: \(enabled)")
// Update MPV's target-colorspace-hint
if mpv != nil {
mpv_set_property_string(mpv, "target-colorspace-hint", enabled ? "yes" : "no")
}
// Re-evaluate EDR mode with current sig-peak
DispatchQueue.main.async {
self.updateEDRMode(sigPeak: self.lastSigPeak)
}
}
func getProperty(_ name: String) -> String? {
guard mpv != nil else { return nil }
let cstr = mpv_get_property_string(mpv, name)
defer { mpv_free(cstr) }
return cstr.map { String(cString: $0) }
}
func setLogLevel(_ level: String) {
guard let mpv = mpv else { return }
mpv_request_log_messages(mpv, level)
}
func observeProperty(_ name: String, format: String) {
guard mpv != nil else { return }
let mpvFormat: mpv_format
switch format {
case "double": mpvFormat = MPV_FORMAT_DOUBLE
case "flag": mpvFormat = MPV_FORMAT_FLAG
case "node": mpvFormat = MPV_FORMAT_NODE
case "string": mpvFormat = MPV_FORMAT_STRING
default: return
}
mpv_observe_property(mpv, 0, name, mpvFormat)
}
func command(_ args: [String]) {
guard mpv != nil, !args.isEmpty else { return }
command(args[0], args: Array(args.dropFirst()))
}
/// Execute an MPV command asynchronously to prevent UI blocking.
/// Uses mpv_command_async which returns immediately; the completion is called
/// when MPV_EVENT_COMMAND_REPLY is received.
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
guard let mpv = mpv, !args.isEmpty else {
completion(.success(()))
return
}
// Generate unique request ID
pendingCommandsLock.lock()
let requestId = nextRequestId
nextRequestId += 1
pendingCommands[requestId] = completion
pendingCommandsLock.unlock()
// Build array of C strings for mpv_command_async
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
cargs.append(nil) // null-terminate
// mpv_command_async returns immediately
cargs.withUnsafeBufferPointer { buffer in
var constPtrs = buffer.map { UnsafePointer($0) }
let result = mpv_command_async(mpv, requestId, &constPtrs)
if result < 0 {
// Command submission failed, complete immediately with error
pendingCommandsLock.lock()
if let pending = pendingCommands.removeValue(forKey: requestId) {
pendingCommandsLock.unlock()
let error = NSError(domain: "mpv", code: Int(result),
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(result))])
DispatchQueue.main.async { pending(.failure(error)) }
} else {
pendingCommandsLock.unlock()
}
}
}
// Free the C strings
for ptr in cargs {
free(ptr)
}
}
// MARK: - Visibility
func setVisible(_ visible: Bool) {
guard let container = containerView else { return }
if visible {
// Re-insert at the bottom of the window view stack
container.removeFromSuperview()
window?.insertSubview(container, at: 0)
}
container.isHidden = !visible
print("[MpvPlayerCore] setVisible(\(visible))")
}
func updateFrame(_ frame: CGRect? = nil) {
guard let metalLayer = metalLayer, let container = containerView else { return }
if let frame = frame {
container.frame = frame
metalLayer.frame = container.bounds
} else if let window = window {
container.frame = window.bounds
metalLayer.frame = container.bounds
}
// Update drawable size for proper scaling
let scale = UIScreen.main.nativeScale
metalLayer.drawableSize = CGSize(
width: metalLayer.frame.width * scale,
height: metalLayer.frame.height * scale
)
print("[MpvPlayerCore] updateFrame: \(container.frame)")
}
// MARK: - Private Helpers
private func command(_ cmd: String, args: [String] = []) {
guard mpv != nil else { return }
// Build array of C strings for mpv_command
var cargs: [UnsafeMutablePointer<CChar>?] = ([cmd] + args).map { strdup($0) }
cargs.append(nil) // null-terminate
defer {
for ptr in cargs {
free(ptr)
}
}
// mpv_command expects UnsafePointer, use withUnsafeBufferPointer for the conversion
cargs.withUnsafeBufferPointer { buffer in
var constPtrs = buffer.map { UnsafePointer($0) }
_ = mpv_command(mpv, &constPtrs)
}
}
private func readEvents() {
queue.async { [weak self] in
guard let self = self, !self.isDisposing, let mpv = self.mpv else { return }
while true {
let event = mpv_wait_event(mpv, 0)
guard let eventPtr = event else { break }
if eventPtr.pointee.event_id == MPV_EVENT_NONE {
break
}
self.handleEvent(eventPtr.pointee)
}
}
}
private func handleEvent(_ event: mpv_event) {
switch event.event_id {
case MPV_EVENT_PROPERTY_CHANGE:
guard let data = event.data else { break }
let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee
let name = String(cString: property.name)
handlePropertyChange(name: name, property: property)
case MPV_EVENT_COMMAND_REPLY:
// Handle async command completion
let requestId = event.reply_userdata
pendingCommandsLock.lock()
let completion = pendingCommands.removeValue(forKey: requestId)
pendingCommandsLock.unlock()
if let completion = completion {
if event.error < 0 {
let error = NSError(domain: "mpv", code: Int(event.error),
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(event.error))])
DispatchQueue.main.async { completion(.failure(error)) }
} else {
DispatchQueue.main.async { completion(.success(())) }
}
}
case MPV_EVENT_FILE_LOADED:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "file-loaded", data: nil)
}
case MPV_EVENT_END_FILE:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "end-file", data: nil)
}
case MPV_EVENT_SHUTDOWN:
print("[MpvPlayerCore] MPV shutdown event")
case MPV_EVENT_PLAYBACK_RESTART:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "playback-restart", data: nil)
}
case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee
let prefix = msg.prefix.map { String(cString: $0) } ?? ""
let level = msg.level.map { String(cString: $0) } ?? ""
let text = msg.text.map { String(cString: $0) } ?? ""
DispatchQueue.main.async {
self.delegate?.onEvent(name: "log-message", data: [
"prefix": prefix,
"level": level,
"text": text
])
}
}
default:
break
}
}
private func handlePropertyChange(name: String, property: mpv_event_property) {
var value: Any?
switch property.format {
case MPV_FORMAT_DOUBLE:
if let ptr = property.data {
value = ptr.assumingMemoryBound(to: Double.self).pointee
}
case MPV_FORMAT_FLAG:
if let ptr = property.data {
value = ptr.assumingMemoryBound(to: Int32.self).pointee != 0
}
case MPV_FORMAT_NODE:
if let ptr = property.data {
let node = ptr.assumingMemoryBound(to: mpv_node.self).pointee
value = convertNode(node)
}
case MPV_FORMAT_STRING:
if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) }
}
default:
break
}
// Handle sig-peak for HDR/EDR activation
if name == "video-params/sig-peak", let sigPeak = value as? Double {
lastSigPeak = sigPeak
DispatchQueue.main.async {
self.updateEDRMode(sigPeak: sigPeak)
}
}
DispatchQueue.main.async {
self.delegate?.onPropertyChange(name: name, value: value)
}
}
// MARK: - HDR/EDR Support
private func updateEDRMode(sigPeak: Double) {
guard let layer = metalLayer else { return }
// Check if screen supports EDR (iOS 16+)
var edrHeadroom: CGFloat = 1.0
if #available(iOS 16.0, *) {
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
}
let isHDRContent = sigPeak > 1.0
let screenSupportsEDR = edrHeadroom > 1.0
let shouldEnableEDR = hdrEnabled && isHDRContent && screenSupportsEDR
if #available(iOS 16.0, *) {
layer.wantsExtendedDynamicRangeContent = shouldEnableEDR
}
print(
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))"
)
}
private func convertNode(_ node: mpv_node) -> Any? {
switch node.format {
case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) }
case MPV_FORMAT_FLAG:
return node.u.flag != 0
case MPV_FORMAT_INT64:
return node.u.int64
case MPV_FORMAT_DOUBLE:
return node.u.double_
case MPV_FORMAT_NODE_ARRAY:
guard let list = node.u.list?.pointee else { return nil }
var array = [Any]()
for i in 0..<Int(list.num) {
if let item = convertNode(list.values[i]) {
array.append(item)
}
}
return array
case MPV_FORMAT_NODE_MAP:
guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]()
for i in 0..<Int(list.num) {
if let key = list.keys?[i].map({ String(cString: $0) }),
let val = convertNode(list.values[i])
{
dict[key] = val
}
}
return dict
default:
return nil
}
}
private func checkError(_ status: CInt) {
if status < 0 {
print("[MpvPlayerCore] MPV error: \(String(cString: mpv_error_string(status)))")
}
}
// MARK: - Cleanup
func dispose() {
// Set disposing flag first to prevent race conditions with event callbacks
isDisposing = true
NotificationCenter.default.removeObserver(self)
// Cancel any pending async commands
pendingCommandsLock.lock()
let pending = pendingCommands
pendingCommands.removeAll()
pendingCommandsLock.unlock()
// Complete pending commands with cancellation error
let cancelError = NSError(domain: "mpv", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Player disposed"])
for (_, completion) in pending {
DispatchQueue.main.async { completion(.failure(cancelError)) }
}
let mpvHandle = mpv
mpv = nil
// Use async to avoid blocking the main thread (prevents deadlock)
queue.async {
if let handle = mpvHandle {
mpv_set_wakeup_callback(handle, nil, nil)
mpv_terminate_destroy(handle)
}
}
metalLayer?.removeFromSuperlayer()
metalLayer = nil
containerView?.removeFromSuperview()
containerView = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
}
+266 -134
View File
@@ -1,51 +1,65 @@
import UIKit
import Flutter
import AVKit
/// Flutter plugin that bridges MPV player to Dart via method and event channels
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginShared {
// MARK: - Properties
private var playerCore: MpvPlayerCore?
private var eventSink: FlutterEventSink?
var eventSink: FlutterEventSink?
private weak var registrar: FlutterPluginRegistrar?
private var nameToId: [String: Int] = [:]
var nameToId: [String: Int] = [:]
// MpvPluginShared conformance
var coreBase: MpvPlayerCoreBase? { playerCore }
func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) }
func updatePlayerFrame() { playerCore?.updateFrame() }
// PiP
private var pipController: MpvPipController?
private var pipChannel: FlutterMethodChannel?
private var autoPipEnabled = false
private var isManualPipRequest = false
private var pipTimebaseSyncTimer: Timer?
private var pendingInlineRestoreAfterPip = false
private var sceneActivationObserverRegistered = false
// MARK: - FlutterPlugin Registration
static func register(with registrar: FlutterPluginRegistrar) {
// Method channel for commands
let methodChannel = FlutterMethodChannel(
name: "com.plezy/mpv_player",
binaryMessenger: registrar.messenger()
)
// Event channel for state updates
let eventChannel = FlutterEventChannel(
name: "com.plezy/mpv_player/events",
binaryMessenger: registrar.messenger()
)
let pipChannel = FlutterMethodChannel(
name: "com.plezy/pip",
binaryMessenger: registrar.messenger()
)
let instance = MpvPlayerPlugin()
instance.registrar = registrar
instance.pipChannel = pipChannel
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
print("[MpvPlayerPlugin] Registered with Flutter")
pipChannel.setMethodCallHandler(instance.handlePipCall)
}
// MARK: - FlutterStreamHandler
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
print("[MpvPlayerPlugin] Event stream connected")
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
print("[MpvPlayerPlugin] Event stream disconnected")
return nil
}
@@ -55,40 +69,198 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
switch call.method {
case "initialize":
handleInitialize(result: result)
case "dispose":
handleDispose(result: result)
case "setProperty":
handleSetProperty(call: call, result: result)
case "getProperty":
handleGetProperty(call: call, result: result)
case "observeProperty":
handleObserveProperty(call: call, result: result)
case "command":
handleCommand(call: call, result: result)
case "setVisible":
handleSetVisible(call: call, result: result)
case "isInitialized":
result(playerCore?.isInitialized ?? false)
case "updateFrame":
handleUpdateFrame(result: result)
case "setLogLevel":
handleSetLogLevel(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - Method Handlers
// MARK: - PiP
private func ensurePipController() -> MpvPipController {
if let existing = pipController { return existing }
let controller = MpvPipController()
controller.delegate = self
pipController = controller
return controller
}
private func registerSceneActivationObserver() {
guard !sceneActivationObserverRegistered else { return }
NotificationCenter.default.addObserver(
self,
selector: #selector(sceneDidActivate),
name: UIScene.didActivateNotification,
object: nil
)
sceneActivationObserverRegistered = true
}
private func unregisterSceneActivationObserver() {
guard sceneActivationObserverRegistered else { return }
NotificationCenter.default.removeObserver(self, name: UIScene.didActivateNotification, object: nil)
sceneActivationObserverRegistered = false
}
private var isSceneActive: Bool {
UIApplication.shared.connectedScenes.contains { $0.activationState == .foregroundActive }
}
private func restoreInlinePlayerAfterPip() {
guard pendingInlineRestoreAfterPip,
let playerCore = playerCore,
!playerCore.isPipActive,
!playerCore.isPipStarting else { return }
print("[MpvPlayerPlugin] Restoring inline player after PiP")
playerCore.setVisible(true)
playerCore.updateFrame()
if playerCore.isPaused {
playerCore.forceDraw()
}
pendingInlineRestoreAfterPip = false
}
private func handlePipCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { result(nil); return }
switch call.method {
case "isSupported":
result(MpvPipController.isSupported)
case "enter":
self.enterPip(manual: true, result: result)
case "setAutoPipReady":
if let args = call.arguments as? [String: Any], let ready = args["ready"] as? Bool {
self.autoPipEnabled = ready
if ready {
let pip = self.ensurePipController()
pip.setAutoStart(true)
// Warm the layer so the system considers PiP possible
if let pc = self.playerCore {
pip.warmLayer(currentTime: pc.timePos, isPlaying: !pc.isPaused)
}
} else {
self.pipController?.setAutoStart(false)
}
}
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}
/// Switch to PiP VO and prepare the sample buffer layer for PiP display.
/// Returns the MpvPipController on success, nil on failure.
@discardableResult
private func switchToPipAndPrepare() -> MpvPipController? {
guard let playerCore = playerCore else { return nil }
let pip = ensurePipController()
guard playerCore.switchToPipVO(layerPtr: pip.layerPointer) else { return nil }
pendingInlineRestoreAfterPip = false
playerCore.isPipStarting = true
pip.pushBlankFrame()
pip.syncTimebase(currentTime: playerCore.timePos, isPlaying: !playerCore.isPaused)
pip.invalidatePlaybackState()
return pip
}
/// Manual PiP entry (button press). Auto-PiP is handled by the system via
/// canStartPictureInPictureAutomaticallyFromInline + pipWillStart delegate.
private func enterPip(manual: Bool, result: FlutterResult? = nil) {
guard MpvPipController.isSupported else {
result?(["success": false, "errorCode": "ios_version", "errorMessage": "Requires iOS 15.0+"])
return
}
guard playerCore != nil else {
result?(["success": false, "errorCode": "failed", "errorMessage": "Player not initialized"])
return
}
guard let pip = switchToPipAndPrepare() else {
result?(["success": false, "errorCode": "vo_switch_failed", "errorMessage": "Failed to switch VO"])
return
}
isManualPipRequest = manual
pip.startPip(waitForFrame: manual) { [weak self] started in
if started {
result?(["success": true])
} else {
self?.cleanupPip(notify: false)
result?(["success": false, "errorCode": "failed", "errorMessage": "PiP failed to start"])
}
}
}
/// Unified cleanup for all PiP exit paths
private func cleanupPip(notify: Bool, pause: Bool = false) {
playerCore?.isPipStarting = false
playerCore?.isPipActive = false
isManualPipRequest = false
stopPipTimebaseSync()
pipController?.flushLayer()
let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false
if pause { playerCore?.setProperty("pause", value: "yes") }
pendingInlineRestoreAfterPip = restoredInlineVO
if pendingInlineRestoreAfterPip {
if isSceneActive {
restoreInlinePlayerAfterPip()
} else {
print("[MpvPlayerPlugin] Deferring inline restore until scene activation")
}
}
if notify { pipChannel?.invokeMethod("onPipChanged", arguments: false) }
}
/// Scene became active restore inline playback if needed and re-warm the
/// sample-buffer layer so future auto-PiP remains possible.
@objc private func sceneDidActivate() {
restoreInlinePlayerAfterPip()
if autoPipEnabled, let pip = pipController, let pc = playerCore, !pc.isPipActive {
pip.warmLayer(currentTime: pc.timePos, isPlaying: !pc.isPaused)
}
}
// MARK: - Timebase Sync
private func syncPipTimebase() {
guard let playerCore = playerCore, let pipController = pipController else { return }
pipController.syncTimebase(
currentTime: playerCore.timePos,
isPlaying: !playerCore.isPaused
)
}
private func startPipTimebaseSync() {
stopPipTimebaseSync()
pipTimebaseSyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.syncPipTimebase()
}
}
private func stopPipTimebaseSync() {
pipTimebaseSyncTimer?.invalidate()
pipTimebaseSyncTimer = nil
}
// MARK: - Platform-Specific Method Handlers
private func handleInitialize(result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
@@ -97,45 +269,43 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
return
}
// Check if already initialized
if self.playerCore?.isInitialized == true {
print("[MpvPlayerPlugin] Already initialized")
self.registerSceneActivationObserver()
result(true)
return
}
// Find the key window
guard let window = self.findKeyWindow() else {
print("[MpvPlayerPlugin] Failed to find key window")
result(FlutterError(code: "NO_WINDOW", message: "Could not find key window", details: nil))
return
}
// Create and initialize player core
let core = MpvPlayerCore()
core.delegate = self
guard core.initialize(in: window) else {
print("[MpvPlayerPlugin] Failed to initialize MPV")
result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil))
return
}
self.playerCore = core
// Start hidden
self.registerSceneActivationObserver()
core.setVisible(false)
print("[MpvPlayerPlugin] Initialized successfully")
result(true)
}
}
private func handleDispose(result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
self?.playerCore?.dispose()
self?.playerCore = nil
print("[MpvPlayerPlugin] Disposed")
guard let self = self else { result(nil); return }
self.pipController?.teardown()
self.pipController = nil
self.autoPipEnabled = false
self.pendingInlineRestoreAfterPip = false
self.unregisterSceneActivationObserver()
self.stopPipTimebaseSync()
self.playerCore?.dispose()
self.playerCore = nil
result(nil)
}
}
@@ -149,109 +319,15 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
}
playerCore?.setProperty(name, value: value)
if name == "pause" {
pipController?.invalidatePlaybackState()
if playerCore?.isPipActive == true { syncPipTimebase() }
}
result(nil)
}
private func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil))
return
}
let value = playerCore?.getProperty(name)
result(value)
}
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String,
let format = args["format"] as? String,
let id = args["id"] as? Int else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
return
}
nameToId[name] = id
playerCore?.observeProperty(name, format: format)
result(nil)
}
private func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let commandArgs = args["args"] as? [String] else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil))
return
}
// Use async command to prevent UI blocking during network operations
playerCore?.commandAsync(commandArgs) { commandResult in
switch commandResult {
case .success:
result(nil)
case .failure(let error):
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
}
} ?? result(nil)
}
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let visible = args["visible"] as? Bool else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
return
}
DispatchQueue.main.async { [weak self] in
self?.playerCore?.setVisible(visible)
// Update frame when becoming visible
if visible {
self?.playerCore?.updateFrame()
}
result(nil)
}
}
private func handleUpdateFrame(result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
self?.playerCore?.updateFrame()
result(nil)
}
}
private func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let level = args["level"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
return
}
playerCore?.setLogLevel(level)
result(nil)
}
// MARK: - MpvPlayerDelegate
func onPropertyChange(name: String, value: Any?) {
guard let eventSink = eventSink else { return }
if let propId = nameToId[name] {
eventSink([propId, value as Any])
}
}
func onEvent(name: String, data: [String: Any]?) {
guard let eventSink = eventSink else { return }
var event: [String: Any] = ["type": "event", "name": name]
if let data = data {
event["data"] = data
}
eventSink(event)
}
// MARK: - Helpers
private func findKeyWindow() -> UIWindow? {
@@ -262,3 +338,59 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
return window
}
}
// MARK: - MpvPipDelegate
extension MpvPlayerPlugin: MpvPipDelegate {
func pipWillStart() {
// If PiP was system-initiated (not via our enterPip), switch VO now
guard let playerCore = playerCore, !playerCore.isPipStarting else { return }
print("[MpvPlayerPlugin] System-initiated PiP detected, switching VO")
if switchToPipAndPrepare() == nil {
print("[MpvPlayerPlugin] VO switch failed for system-initiated PiP")
pipController?.stopPip()
}
}
func pipDidStart() {
playerCore?.isPipStarting = false
playerCore?.isPipActive = true
pendingInlineRestoreAfterPip = false
pipChannel?.invokeMethod("onPipChanged", arguments: true)
syncPipTimebase()
startPipTimebaseSync()
if isManualPipRequest {
isManualPipRequest = false
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
}
}
func pipDidStop(restored: Bool) {
cleanupPip(notify: true, pause: !restored)
}
func pipDidFailToStart(error: Error?) {
cleanupPip(notify: true)
}
func pipSetPlaying(_ playing: Bool) {
playerCore?.setProperty("pause", value: playing ? "no" : "yes")
pipController?.invalidatePlaybackState()
syncPipTimebase()
}
func pipSkip(byInterval seconds: Double) {
guard let playerCore = playerCore else { return }
let newTime = max(0, playerCore.timePos + seconds)
playerCore.command(["seek", String(newTime), "absolute"])
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.syncPipTimebase()
self?.pipController?.invalidatePlaybackState()
}
}
var isPipPlaying: Bool { !(playerCore?.isPaused ?? true) }
var pipDuration: Double { playerCore?.duration ?? 0 }
}
+3 -1
View File
@@ -6,8 +6,10 @@ default_platform(:ios)
platform :ios do
desc "Build and deploy to App Store"
lane :deploy_appstore do
git_commit = `git rev-parse --short HEAD`.strip
# Build the Flutter app
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true")
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=app-store")
# Upload to App Store
upload_to_app_store(
File diff suppressed because one or more lines are too long
+335
View File
@@ -0,0 +1,335 @@
// Generated from USB HID Usage Tables — do not edit by hand.
/// Human-readable labels for physical keyboard keys, keyed by USB HID usage code.
const hidKeyLabels = <int, String>{
// Special
0x00000010: 'Hyper',
0x00000011: 'Super',
0x00000012: 'Fn',
0x00000013: 'Fn Lock',
0x00000014: 'Suspend',
0x00000015: 'Resume',
0x00000016: 'Turbo',
0x00000017: 'Privacy Screen Toggle',
0x00000018: 'Microphone Mute',
// Generic Desktop
0x00010082: 'Sleep',
0x00010083: 'Wake Up',
0x000100b5: 'Display Toggle',
// Game Buttons
0x0005ff01: 'Game Button 1',
0x0005ff02: 'Game Button 2',
0x0005ff03: 'Game Button 3',
0x0005ff04: 'Game Button 4',
0x0005ff05: 'Game Button 5',
0x0005ff06: 'Game Button 6',
0x0005ff07: 'Game Button 7',
0x0005ff08: 'Game Button 8',
0x0005ff09: 'Game Button 9',
0x0005ff0a: 'Game Button 10',
0x0005ff0b: 'Game Button 11',
0x0005ff0c: 'Game Button 12',
0x0005ff0d: 'Game Button 13',
0x0005ff0e: 'Game Button 14',
0x0005ff0f: 'Game Button 15',
0x0005ff10: 'Game Button 16',
0x0005ff11: 'Game Button A',
0x0005ff12: 'Game Button B',
0x0005ff13: 'Game Button C',
0x0005ff14: 'Game Button Left 1',
0x0005ff15: 'Game Button Left 2',
0x0005ff16: 'Game Button Mode',
0x0005ff17: 'Game Button Right 1',
0x0005ff18: 'Game Button Right 2',
0x0005ff19: 'Game Button Select',
0x0005ff1a: 'Game Button Start',
0x0005ff1b: 'Game Button Thumb Left',
0x0005ff1c: 'Game Button Thumb Right',
0x0005ff1d: 'Game Button X',
0x0005ff1e: 'Game Button Y',
0x0005ff1f: 'Game Button Z',
// Keyboard/Keypad
0x00070004: 'A',
0x00070005: 'B',
0x00070006: 'C',
0x00070007: 'D',
0x00070008: 'E',
0x00070009: 'F',
0x0007000a: 'G',
0x0007000b: 'H',
0x0007000c: 'I',
0x0007000d: 'J',
0x0007000e: 'K',
0x0007000f: 'L',
0x00070010: 'M',
0x00070011: 'N',
0x00070012: 'O',
0x00070013: 'P',
0x00070014: 'Q',
0x00070015: 'R',
0x00070016: 'S',
0x00070017: 'T',
0x00070018: 'U',
0x00070019: 'V',
0x0007001a: 'W',
0x0007001b: 'X',
0x0007001c: 'Y',
0x0007001d: 'Z',
0x0007001e: '1',
0x0007001f: '2',
0x00070020: '3',
0x00070021: '4',
0x00070022: '5',
0x00070023: '6',
0x00070024: '7',
0x00070025: '8',
0x00070026: '9',
0x00070027: '0',
0x00070028: 'Enter',
0x00070029: 'Escape',
0x0007002a: 'Backspace',
0x0007002b: 'Tab',
0x0007002c: 'Space',
0x0007002d: '-',
0x0007002e: '=',
0x0007002f: '[',
0x00070030: ']',
0x00070031: r'\',
0x00070033: ';',
0x00070034: "'",
0x00070035: '`',
0x00070036: ',',
0x00070037: '.',
0x00070038: '/',
0x00070039: 'Caps Lock',
0x0007003a: 'F1',
0x0007003b: 'F2',
0x0007003c: 'F3',
0x0007003d: 'F4',
0x0007003e: 'F5',
0x0007003f: 'F6',
0x00070040: 'F7',
0x00070041: 'F8',
0x00070042: 'F9',
0x00070043: 'F10',
0x00070044: 'F11',
0x00070045: 'F12',
0x00070046: 'Print Screen',
0x00070047: 'Scroll Lock',
0x00070048: 'Pause',
0x00070049: 'Insert',
0x0007004a: 'Home',
0x0007004b: 'Page Up',
0x0007004c: 'Delete',
0x0007004d: 'End',
0x0007004e: 'Page Down',
0x0007004f: 'Arrow Right',
0x00070050: 'Arrow Left',
0x00070051: 'Arrow Down',
0x00070052: 'Arrow Up',
0x00070053: 'Num Lock',
0x00070054: 'Numpad /',
0x00070055: 'Numpad *',
0x00070056: 'Numpad -',
0x00070057: 'Numpad +',
0x00070058: 'Numpad Enter',
0x00070059: 'Numpad 1',
0x0007005a: 'Numpad 2',
0x0007005b: 'Numpad 3',
0x0007005c: 'Numpad 4',
0x0007005d: 'Numpad 5',
0x0007005e: 'Numpad 6',
0x0007005f: 'Numpad 7',
0x00070060: 'Numpad 8',
0x00070061: 'Numpad 9',
0x00070062: 'Numpad 0',
0x00070063: 'Numpad .',
0x00070064: 'Intl Backslash',
0x00070065: 'Context Menu',
0x00070066: 'Power',
0x00070067: 'Numpad =',
0x00070068: 'F13',
0x00070069: 'F14',
0x0007006a: 'F15',
0x0007006b: 'F16',
0x0007006c: 'F17',
0x0007006d: 'F18',
0x0007006e: 'F19',
0x0007006f: 'F20',
0x00070070: 'F21',
0x00070071: 'F22',
0x00070072: 'F23',
0x00070073: 'F24',
0x00070074: 'Open',
0x00070075: 'Help',
0x00070077: 'Select',
0x00070079: 'Again',
0x0007007a: 'Undo',
0x0007007b: 'Cut',
0x0007007c: 'Copy',
0x0007007d: 'Paste',
0x0007007e: 'Find',
0x0007007f: 'Volume Mute',
0x00070080: 'Volume Up',
0x00070081: 'Volume Down',
0x00070085: 'Numpad ,',
0x00070087: 'Intl Ro',
0x00070088: 'Kana Mode',
0x00070089: 'Intl Yen',
0x0007008a: 'Convert',
0x0007008b: 'Non Convert',
0x00070090: 'Lang 1',
0x00070091: 'Lang 2',
0x00070092: 'Lang 3',
0x00070093: 'Lang 4',
0x00070094: 'Lang 5',
0x0007009b: 'Abort',
0x000700a3: 'Props',
0x000700b6: 'Numpad (',
0x000700b7: 'Numpad )',
0x000700bb: 'Numpad Backspace',
0x000700d0: 'Numpad Memory Store',
0x000700d1: 'Numpad Memory Recall',
0x000700d2: 'Numpad Memory Clear',
0x000700d3: 'Numpad Memory Add',
0x000700d4: 'Numpad Memory Subtract',
0x000700d7: 'Numpad Sign Change',
0x000700d8: 'Numpad Clear',
0x000700d9: 'Numpad Clear Entry',
0x000700e0: 'Ctrl',
0x000700e1: 'Shift',
0x000700e2: 'Alt',
0x000700e3: 'Meta',
0x000700e4: 'Ctrl',
0x000700e5: 'Shift',
0x000700e6: 'Alt',
0x000700e7: 'Meta',
// Consumer
0x000c0030: 'Power',
0x000c0032: 'Sleep',
0x000c0040: 'Menu',
0x000c0041: 'Menu Pick',
0x000c0042: 'Menu Up',
0x000c0043: 'Menu Down',
0x000c0044: 'Menu Left',
0x000c0045: 'Menu Right',
0x000c0046: 'Menu Escape',
0x000c0047: 'Menu Value Increase',
0x000c0048: 'Menu Value Decrease',
0x000c0060: 'Info',
0x000c0061: 'Closed Caption',
0x000c0062: 'Closed Caption Select',
0x000c0063: 'VCR/TV',
0x000c0064: 'Broadcast Mode',
0x000c0065: 'Snapshot',
0x000c0066: 'Still',
0x000c0067: 'PiP Toggle',
0x000c0068: 'PiP Swap',
0x000c0069: 'Red Button',
0x000c006a: 'Green Button',
0x000c006b: 'Blue Button',
0x000c006c: 'Yellow Button',
0x000c006d: 'Aspect',
0x000c006e: '3D Mode Select',
0x000c006f: 'Brightness Up',
0x000c0070: 'Brightness Down',
0x000c0072: 'Brightness Toggle',
0x000c0073: 'Brightness Min',
0x000c0074: 'Brightness Max',
0x000c0075: 'Brightness Auto',
0x000c0079: 'Kbd Illum Up',
0x000c007a: 'Kbd Illum Down',
0x000c0082: 'Mode Step',
0x000c0083: 'Recall Last',
0x000c0084: 'Enter Channel',
0x000c0085: 'Order Movie',
0x000c0086: 'Channel',
0x000c0088: 'Media Computer',
0x000c0089: 'Media TV',
0x000c008a: 'Media WWW',
0x000c008b: 'Media DVD',
0x000c008c: 'Media Telephone',
0x000c008d: 'Program Guide',
0x000c008e: 'Media Video Phone',
0x000c008f: 'Media Games',
0x000c0090: 'Media Messages',
0x000c0091: 'Media CD',
0x000c0092: 'Media VCR',
0x000c0093: 'Media Tuner',
0x000c0094: 'Quit',
0x000c0095: 'Help',
0x000c0096: 'Media Tape',
0x000c0097: 'Media Cable',
0x000c0098: 'Media Satellite',
0x000c0099: 'Media Security',
0x000c009a: 'Media Home',
0x000c009b: 'Media Call',
0x000c009c: 'Channel Up',
0x000c009d: 'Channel Down',
0x000c009e: 'Media SAP',
0x000c00b0: 'Play',
0x000c00b1: 'Pause',
0x000c00b2: 'Record',
0x000c00b3: 'Fast Forward',
0x000c00b4: 'Rewind',
0x000c00b5: 'Next Track',
0x000c00b6: 'Previous Track',
0x000c00b7: 'Stop',
0x000c00b8: 'Eject',
0x000c00b9: 'Random Play',
0x000c00ba: 'Select Disc',
0x000c00bb: 'Enter Disc',
0x000c00bc: 'Repeat',
0x000c00c0: 'Frame Forward',
0x000c00c1: 'Frame Back',
0x000c00cc: 'Stop/Eject',
0x000c00cd: 'Play/Pause',
0x000c00ce: 'Play/Skip',
0x000c00cf: 'Voice Command',
0x000c00e0: 'Volume',
0x000c00e2: 'Mute',
0x000c00e5: 'Bass Boost',
0x000c00e6: 'Surround Mode',
0x000c00e7: 'Loudness',
0x000c00e9: 'Volume Up',
0x000c00ea: 'Volume Down',
0x000c0183: 'Media Select',
0x000c0184: 'Launch Word Processor',
0x000c0186: 'Launch Spreadsheet',
0x000c018a: 'Launch Mail',
0x000c018d: 'Launch Contacts',
0x000c018e: 'Launch Calendar',
0x000c0192: 'Launch Calculator',
0x000c0194: 'Launch File Browser',
0x000c0196: 'Launch Browser',
0x000c019c: 'Log Off',
0x000c019e: 'Lock Screen',
0x000c019f: 'Launch Control Panel',
0x000c01a2: 'Select Task',
0x000c01a7: 'Launch Documents',
0x000c01ab: 'Spell Check',
0x000c01ae: 'Launch Keyboard Layout',
0x000c01b1: 'Launch Screen Saver',
0x000c01b7: 'Launch Audio Browser',
0x000c01cb: 'Launch Assistant',
0x000c0201: 'New',
0x000c0203: 'Close',
0x000c0207: 'Save',
0x000c0208: 'Print',
0x000c0221: 'Browser Search',
0x000c0223: 'Browser Home',
0x000c0224: 'Browser Back',
0x000c0225: 'Browser Forward',
0x000c0226: 'Browser Stop',
0x000c0227: 'Browser Refresh',
0x000c022a: 'Browser Favorites',
0x000c022d: 'Zoom In',
0x000c022e: 'Zoom Out',
0x000c0232: 'Zoom Toggle',
0x000c0279: 'Redo',
0x000c0289: 'Mail Reply',
0x000c028b: 'Mail Forward',
0x000c028c: 'Mail Send',
0x000c029d: 'Keyboard Layout Select',
0x000c029f: 'Show All Windows',
};
File diff suppressed because one or more lines are too long
+406
View File
@@ -0,0 +1,406 @@
// Generated from iso_639_codes.json — do not edit by hand.
class LanguageEntry {
final String code1;
final String code2;
final String? code2B;
final String name;
const LanguageEntry(this.code1, this.code2, this.code2B, this.name);
}
const languageEntries = <String, LanguageEntry>{
'aa': LanguageEntry('aa', 'aar', null, 'Afar'),
'ab': LanguageEntry('ab', 'abk', null, 'Abkhaz'),
'ae': LanguageEntry('ae', 'ave', null, 'Avestan'),
'af': LanguageEntry('af', 'afr', null, 'Afrikaans'),
'ak': LanguageEntry('ak', 'aka', null, 'Akan'),
'am': LanguageEntry('am', 'amh', null, 'Amharic'),
'an': LanguageEntry('an', 'arg', null, 'Aragonese'),
'ar': LanguageEntry('ar', 'ara', null, 'Arabic'),
'as': LanguageEntry('as', 'asm', null, 'Assamese'),
'av': LanguageEntry('av', 'ava', null, 'Avaric'),
'ay': LanguageEntry('ay', 'aym', null, 'Aymara'),
'az': LanguageEntry('az', 'aze', null, 'Azerbaijani'),
'ba': LanguageEntry('ba', 'bak', null, 'Bashkir'),
'be': LanguageEntry('be', 'bel', null, 'Belarusian'),
'bg': LanguageEntry('bg', 'bul', null, 'Bulgarian'),
'bh': LanguageEntry('bh', 'bih', null, 'Bihari'),
'bi': LanguageEntry('bi', 'bis', null, 'Bislama'),
'bm': LanguageEntry('bm', 'bam', null, 'Bambara'),
'bn': LanguageEntry('bn', 'ben', null, 'Bengali, Bangla'),
'bo': LanguageEntry('bo', 'bod', 'tib', 'Tibetan Standard, Tibetan, Central'),
'br': LanguageEntry('br', 'bre', null, 'Breton'),
'bs': LanguageEntry('bs', 'bos', null, 'Bosnian'),
'ca': LanguageEntry('ca', 'cat', null, 'Catalan'),
'ce': LanguageEntry('ce', 'che', null, 'Chechen'),
'ch': LanguageEntry('ch', 'cha', null, 'Chamorro'),
'co': LanguageEntry('co', 'cos', null, 'Corsican'),
'cr': LanguageEntry('cr', 'cre', null, 'Cree'),
'cs': LanguageEntry('cs', 'ces', 'cze', 'Czech'),
'cu': LanguageEntry('cu', 'chu', null, 'Old Church Slavonic, Church Slavonic, Old Bulgarian'),
'cv': LanguageEntry('cv', 'chv', null, 'Chuvash'),
'cy': LanguageEntry('cy', 'cym', 'wel', 'Welsh'),
'da': LanguageEntry('da', 'dan', null, 'Danish'),
'de': LanguageEntry('de', 'deu', 'ger', 'German'),
'dv': LanguageEntry('dv', 'div', null, 'Divehi, Dhivehi, Maldivian'),
'dz': LanguageEntry('dz', 'dzo', null, 'Dzongkha'),
'ee': LanguageEntry('ee', 'ewe', null, 'Ewe'),
'el': LanguageEntry('el', 'ell', 'gre', 'Greek (modern)'),
'en': LanguageEntry('en', 'eng', null, 'English'),
'eo': LanguageEntry('eo', 'epo', null, 'Esperanto'),
'es': LanguageEntry('es', 'spa', null, 'Spanish'),
'et': LanguageEntry('et', 'est', null, 'Estonian'),
'eu': LanguageEntry('eu', 'eus', 'baq', 'Basque'),
'fa': LanguageEntry('fa', 'fas', 'per', 'Persian (Farsi)'),
'ff': LanguageEntry('ff', 'ful', null, 'Fula, Fulah, Pulaar, Pular'),
'fi': LanguageEntry('fi', 'fin', null, 'Finnish'),
'fj': LanguageEntry('fj', 'fij', null, 'Fijian'),
'fo': LanguageEntry('fo', 'fao', null, 'Faroese'),
'fr': LanguageEntry('fr', 'fra', 'fre', 'French'),
'fy': LanguageEntry('fy', 'fry', null, 'Western Frisian'),
'ga': LanguageEntry('ga', 'gle', null, 'Irish'),
'gd': LanguageEntry('gd', 'gla', null, 'Scottish Gaelic, Gaelic'),
'gl': LanguageEntry('gl', 'glg', null, 'Galician'),
'gn': LanguageEntry('gn', 'grn', null, 'Guaraní'),
'gu': LanguageEntry('gu', 'guj', null, 'Gujarati'),
'gv': LanguageEntry('gv', 'glv', null, 'Manx'),
'ha': LanguageEntry('ha', 'hau', null, 'Hausa'),
'he': LanguageEntry('he', 'heb', null, 'Hebrew (modern)'),
'hi': LanguageEntry('hi', 'hin', null, 'Hindi'),
'ho': LanguageEntry('ho', 'hmo', null, 'Hiri Motu'),
'hr': LanguageEntry('hr', 'hrv', null, 'Croatian'),
'ht': LanguageEntry('ht', 'hat', null, 'Haitian, Haitian Creole'),
'hu': LanguageEntry('hu', 'hun', null, 'Hungarian'),
'hy': LanguageEntry('hy', 'hye', 'arm', 'Armenian'),
'hz': LanguageEntry('hz', 'her', null, 'Herero'),
'ia': LanguageEntry('ia', 'ina', null, 'Interlingua'),
'id': LanguageEntry('id', 'ind', null, 'Indonesian'),
'ie': LanguageEntry('ie', 'ile', null, 'Interlingue'),
'ig': LanguageEntry('ig', 'ibo', null, 'Igbo'),
'ii': LanguageEntry('ii', 'iii', null, 'Nuosu'),
'ik': LanguageEntry('ik', 'ipk', null, 'Inupiaq'),
'io': LanguageEntry('io', 'ido', null, 'Ido'),
'is': LanguageEntry('is', 'isl', 'ice', 'Icelandic'),
'it': LanguageEntry('it', 'ita', null, 'Italian'),
'iu': LanguageEntry('iu', 'iku', null, 'Inuktitut'),
'ja': LanguageEntry('ja', 'jpn', null, 'Japanese'),
'jv': LanguageEntry('jv', 'jav', null, 'Javanese'),
'ka': LanguageEntry('ka', 'kat', 'geo', 'Georgian'),
'kg': LanguageEntry('kg', 'kon', null, 'Kongo'),
'ki': LanguageEntry('ki', 'kik', null, 'Kikuyu, Gikuyu'),
'kj': LanguageEntry('kj', 'kua', null, 'Kwanyama, Kuanyama'),
'kk': LanguageEntry('kk', 'kaz', null, 'Kazakh'),
'kl': LanguageEntry('kl', 'kal', null, 'Kalaallisut, Greenlandic'),
'km': LanguageEntry('km', 'khm', null, 'Khmer'),
'kn': LanguageEntry('kn', 'kan', null, 'Kannada'),
'ko': LanguageEntry('ko', 'kor', null, 'Korean'),
'kr': LanguageEntry('kr', 'kau', null, 'Kanuri'),
'ks': LanguageEntry('ks', 'kas', null, 'Kashmiri'),
'ku': LanguageEntry('ku', 'kur', null, 'Kurdish'),
'kv': LanguageEntry('kv', 'kom', null, 'Komi'),
'kw': LanguageEntry('kw', 'cor', null, 'Cornish'),
'ky': LanguageEntry('ky', 'kir', null, 'Kyrgyz'),
'la': LanguageEntry('la', 'lat', null, 'Latin'),
'lb': LanguageEntry('lb', 'ltz', null, 'Luxembourgish, Letzeburgesch'),
'lg': LanguageEntry('lg', 'lug', null, 'Ganda'),
'li': LanguageEntry('li', 'lim', null, 'Limburgish, Limburgan, Limburger'),
'ln': LanguageEntry('ln', 'lin', null, 'Lingala'),
'lo': LanguageEntry('lo', 'lao', null, 'Lao'),
'lt': LanguageEntry('lt', 'lit', null, 'Lithuanian'),
'lu': LanguageEntry('lu', 'lub', null, 'Luba-Katanga'),
'lv': LanguageEntry('lv', 'lav', null, 'Latvian'),
'mg': LanguageEntry('mg', 'mlg', null, 'Malagasy'),
'mh': LanguageEntry('mh', 'mah', null, 'Marshallese'),
'mi': LanguageEntry('mi', 'mri', 'mao', 'Māori'),
'mk': LanguageEntry('mk', 'mkd', 'mac', 'Macedonian'),
'ml': LanguageEntry('ml', 'mal', null, 'Malayalam'),
'mn': LanguageEntry('mn', 'mon', null, 'Mongolian'),
'mr': LanguageEntry('mr', 'mar', null, 'Marathi (Marāṭhī)'),
'ms': LanguageEntry('ms', 'msa', 'may', 'Malay'),
'mt': LanguageEntry('mt', 'mlt', null, 'Maltese'),
'my': LanguageEntry('my', 'mya', 'bur', 'Burmese'),
'na': LanguageEntry('na', 'nau', null, 'Nauruan'),
'nb': LanguageEntry('nb', 'nob', null, 'Norwegian Bokmål'),
'nd': LanguageEntry('nd', 'nde', null, 'Northern Ndebele'),
'ne': LanguageEntry('ne', 'nep', null, 'Nepali'),
'ng': LanguageEntry('ng', 'ndo', null, 'Ndonga'),
'nl': LanguageEntry('nl', 'nld', 'dut', 'Dutch'),
'nn': LanguageEntry('nn', 'nno', null, 'Norwegian Nynorsk'),
'no': LanguageEntry('no', 'nor', null, 'Norwegian'),
'nr': LanguageEntry('nr', 'nbl', null, 'Southern Ndebele'),
'nv': LanguageEntry('nv', 'nav', null, 'Navajo, Navaho'),
'ny': LanguageEntry('ny', 'nya', null, 'Chichewa, Chewa, Nyanja'),
'oc': LanguageEntry('oc', 'oci', null, 'Occitan'),
'oj': LanguageEntry('oj', 'oji', null, 'Ojibwe, Ojibwa'),
'om': LanguageEntry('om', 'orm', null, 'Oromo'),
'or': LanguageEntry('or', 'ori', null, 'Oriya'),
'os': LanguageEntry('os', 'oss', null, 'Ossetian, Ossetic'),
'pa': LanguageEntry('pa', 'pan', null, '(Eastern) Punjabi'),
'pi': LanguageEntry('pi', 'pli', null, 'Pāli'),
'pl': LanguageEntry('pl', 'pol', null, 'Polish'),
'ps': LanguageEntry('ps', 'pus', null, 'Pashto, Pushto'),
'pt': LanguageEntry('pt', 'por', null, 'Portuguese'),
'qu': LanguageEntry('qu', 'que', null, 'Quechua'),
'rm': LanguageEntry('rm', 'roh', null, 'Romansh'),
'rn': LanguageEntry('rn', 'run', null, 'Kirundi'),
'ro': LanguageEntry('ro', 'ron', 'rum', 'Romanian'),
'ru': LanguageEntry('ru', 'rus', null, 'Russian'),
'rw': LanguageEntry('rw', 'kin', null, 'Kinyarwanda'),
'sa': LanguageEntry('sa', 'san', null, 'Sanskrit (Saṁskṛta)'),
'sc': LanguageEntry('sc', 'srd', null, 'Sardinian'),
'sd': LanguageEntry('sd', 'snd', null, 'Sindhi'),
'se': LanguageEntry('se', 'sme', null, 'Northern Sami'),
'sg': LanguageEntry('sg', 'sag', null, 'Sango'),
'si': LanguageEntry('si', 'sin', null, 'Sinhalese, Sinhala'),
'sk': LanguageEntry('sk', 'slk', 'slo', 'Slovak'),
'sl': LanguageEntry('sl', 'slv', null, 'Slovene'),
'sm': LanguageEntry('sm', 'smo', null, 'Samoan'),
'sn': LanguageEntry('sn', 'sna', null, 'Shona'),
'so': LanguageEntry('so', 'som', null, 'Somali'),
'sq': LanguageEntry('sq', 'sqi', 'alb', 'Albanian'),
'sr': LanguageEntry('sr', 'srp', null, 'Serbian'),
'ss': LanguageEntry('ss', 'ssw', null, 'Swati'),
'st': LanguageEntry('st', 'sot', null, 'Southern Sotho'),
'su': LanguageEntry('su', 'sun', null, 'Sundanese'),
'sv': LanguageEntry('sv', 'swe', null, 'Swedish'),
'sw': LanguageEntry('sw', 'swa', null, 'Swahili'),
'ta': LanguageEntry('ta', 'tam', null, 'Tamil'),
'te': LanguageEntry('te', 'tel', null, 'Telugu'),
'tg': LanguageEntry('tg', 'tgk', null, 'Tajik'),
'th': LanguageEntry('th', 'tha', null, 'Thai'),
'ti': LanguageEntry('ti', 'tir', null, 'Tigrinya'),
'tk': LanguageEntry('tk', 'tuk', null, 'Turkmen'),
'tl': LanguageEntry('tl', 'tgl', null, 'Tagalog'),
'tn': LanguageEntry('tn', 'tsn', null, 'Tswana'),
'to': LanguageEntry('to', 'ton', null, 'Tonga (Tonga Islands)'),
'tr': LanguageEntry('tr', 'tur', null, 'Turkish'),
'ts': LanguageEntry('ts', 'tso', null, 'Tsonga'),
'tt': LanguageEntry('tt', 'tat', null, 'Tatar'),
'tw': LanguageEntry('tw', 'twi', null, 'Twi'),
'ty': LanguageEntry('ty', 'tah', null, 'Tahitian'),
'ug': LanguageEntry('ug', 'uig', null, 'Uyghur'),
'uk': LanguageEntry('uk', 'ukr', null, 'Ukrainian'),
'ur': LanguageEntry('ur', 'urd', null, 'Urdu'),
'uz': LanguageEntry('uz', 'uzb', null, 'Uzbek'),
've': LanguageEntry('ve', 'ven', null, 'Venda'),
'vi': LanguageEntry('vi', 'vie', null, 'Vietnamese'),
'vo': LanguageEntry('vo', 'vol', null, 'Volapük'),
'wa': LanguageEntry('wa', 'wln', null, 'Walloon'),
'wo': LanguageEntry('wo', 'wol', null, 'Wolof'),
'xh': LanguageEntry('xh', 'xho', null, 'Xhosa'),
'yi': LanguageEntry('yi', 'yid', null, 'Yiddish'),
'yo': LanguageEntry('yo', 'yor', null, 'Yoruba'),
'za': LanguageEntry('za', 'zha', null, 'Zhuang, Chuang'),
'zh': LanguageEntry('zh', 'zho', 'chi', 'Chinese'),
'zu': LanguageEntry('zu', 'zul', null, 'Zulu'),
};
const code2ToCode1 = <String, String>{
'aar': 'aa',
'abk': 'ab',
'ave': 'ae',
'afr': 'af',
'aka': 'ak',
'amh': 'am',
'arg': 'an',
'ara': 'ar',
'asm': 'as',
'ava': 'av',
'aym': 'ay',
'aze': 'az',
'bak': 'ba',
'bel': 'be',
'bul': 'bg',
'bih': 'bh',
'bis': 'bi',
'bam': 'bm',
'ben': 'bn',
'bod': 'bo',
'bre': 'br',
'bos': 'bs',
'cat': 'ca',
'che': 'ce',
'cha': 'ch',
'cos': 'co',
'cre': 'cr',
'ces': 'cs',
'chu': 'cu',
'chv': 'cv',
'cym': 'cy',
'dan': 'da',
'deu': 'de',
'div': 'dv',
'dzo': 'dz',
'ewe': 'ee',
'ell': 'el',
'eng': 'en',
'epo': 'eo',
'spa': 'es',
'est': 'et',
'eus': 'eu',
'fas': 'fa',
'ful': 'ff',
'fin': 'fi',
'fij': 'fj',
'fao': 'fo',
'fra': 'fr',
'fry': 'fy',
'gle': 'ga',
'gla': 'gd',
'glg': 'gl',
'grn': 'gn',
'guj': 'gu',
'glv': 'gv',
'hau': 'ha',
'heb': 'he',
'hin': 'hi',
'hmo': 'ho',
'hrv': 'hr',
'hat': 'ht',
'hun': 'hu',
'hye': 'hy',
'her': 'hz',
'ina': 'ia',
'ind': 'id',
'ile': 'ie',
'ibo': 'ig',
'iii': 'ii',
'ipk': 'ik',
'ido': 'io',
'isl': 'is',
'ita': 'it',
'iku': 'iu',
'jpn': 'ja',
'jav': 'jv',
'kat': 'ka',
'kon': 'kg',
'kik': 'ki',
'kua': 'kj',
'kaz': 'kk',
'kal': 'kl',
'khm': 'km',
'kan': 'kn',
'kor': 'ko',
'kau': 'kr',
'kas': 'ks',
'kur': 'ku',
'kom': 'kv',
'cor': 'kw',
'kir': 'ky',
'lat': 'la',
'ltz': 'lb',
'lug': 'lg',
'lim': 'li',
'lin': 'ln',
'lao': 'lo',
'lit': 'lt',
'lub': 'lu',
'lav': 'lv',
'mlg': 'mg',
'mah': 'mh',
'mri': 'mi',
'mkd': 'mk',
'mal': 'ml',
'mon': 'mn',
'mar': 'mr',
'msa': 'ms',
'mlt': 'mt',
'mya': 'my',
'nau': 'na',
'nob': 'nb',
'nde': 'nd',
'nep': 'ne',
'ndo': 'ng',
'nld': 'nl',
'nno': 'nn',
'nor': 'no',
'nbl': 'nr',
'nav': 'nv',
'nya': 'ny',
'oci': 'oc',
'oji': 'oj',
'orm': 'om',
'ori': 'or',
'oss': 'os',
'pan': 'pa',
'pli': 'pi',
'pol': 'pl',
'pus': 'ps',
'por': 'pt',
'que': 'qu',
'roh': 'rm',
'run': 'rn',
'ron': 'ro',
'rus': 'ru',
'kin': 'rw',
'san': 'sa',
'srd': 'sc',
'snd': 'sd',
'sme': 'se',
'sag': 'sg',
'sin': 'si',
'slk': 'sk',
'slv': 'sl',
'smo': 'sm',
'sna': 'sn',
'som': 'so',
'sqi': 'sq',
'srp': 'sr',
'ssw': 'ss',
'sot': 'st',
'sun': 'su',
'swe': 'sv',
'swa': 'sw',
'tam': 'ta',
'tel': 'te',
'tgk': 'tg',
'tha': 'th',
'tir': 'ti',
'tuk': 'tk',
'tgl': 'tl',
'tsn': 'tn',
'ton': 'to',
'tur': 'tr',
'tso': 'ts',
'tat': 'tt',
'twi': 'tw',
'tah': 'ty',
'uig': 'ug',
'ukr': 'uk',
'urd': 'ur',
'uzb': 'uz',
'ven': 've',
'vie': 'vi',
'vol': 'vo',
'wln': 'wa',
'wol': 'wo',
'xho': 'xh',
'yid': 'yi',
'yor': 'yo',
'zha': 'za',
'zho': 'zh',
'zul': 'zu',
};
const code2BToCode1 = <String, String>{
'tib': 'bo',
'cze': 'cs',
'wel': 'cy',
'ger': 'de',
'gre': 'el',
'baq': 'eu',
'per': 'fa',
'fre': 'fr',
'arm': 'hy',
'ice': 'is',
'geo': 'ka',
'mao': 'mi',
'mac': 'mk',
'may': 'ms',
'bur': 'my',
'dut': 'nl',
'rum': 'ro',
'slo': 'sk',
'alb': 'sq',
'chi': 'zh',
};
+100 -3
View File
@@ -12,12 +12,12 @@ import '../utils/global_key_utils.dart';
part 'app_database.g.dart';
// Simplified database with API cache for offline support
@DriftDatabase(tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress])
@DriftDatabase(tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress, SyncRules])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 8; // Added bgTaskId column to DownloadedMedia
int get schemaVersion => 12;
@override
MigrationStrategy get migration {
@@ -38,6 +38,34 @@ class AppDatabase extends _$AppDatabase {
appLogger.w('bgTaskId column may already exist: $e');
}
}
if (from < 9) {
appLogger.i('Adding mediaIndex column to DownloadedMedia (v9 migration)');
try {
await m.addColumn(downloadedMedia, downloadedMedia.mediaIndex);
} catch (e) {
appLogger.w('mediaIndex column may already exist: $e');
}
}
if (from < 10) {
appLogger.i('Adding SyncRules table (v10 migration)');
await m.createTable(syncRules);
}
if (from < 11) {
appLogger.i('Adding enabled column to SyncRules (v11 migration)');
try {
await m.addColumn(syncRules, syncRules.enabled);
} catch (e) {
appLogger.w('enabled column may already exist: $e');
}
}
if (from < 12) {
appLogger.i('Adding downloadFilter column to SyncRules (v12 migration)');
try {
await m.addColumn(syncRules, syncRules.downloadFilter);
} catch (e) {
appLogger.w('downloadFilter column may already exist: $e');
}
}
},
);
}
@@ -193,6 +221,69 @@ class AppDatabase extends _$AppDatabase {
return delete(offlineWatchProgress).go();
}
// ============================================================
// Sync Rules Operations
// ============================================================
Future<List<SyncRuleItem>> getSyncRules() {
return select(syncRules).get();
}
Future<SyncRuleItem?> getSyncRule(String globalKey) {
return (select(syncRules)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull();
}
Future<void> insertSyncRule({
required String serverId,
required String ratingKey,
required String globalKey,
required String targetType,
required int episodeCount,
int mediaIndex = 0,
String downloadFilter = 'unwatched',
}) async {
await into(syncRules).insertOnConflictUpdate(
SyncRulesCompanion.insert(
serverId: serverId,
ratingKey: ratingKey,
globalKey: globalKey,
targetType: targetType,
episodeCount: episodeCount,
createdAt: DateTime.now().millisecondsSinceEpoch,
mediaIndex: Value(mediaIndex),
downloadFilter: Value(downloadFilter),
),
);
}
Future<void> updateSyncRuleCount(String globalKey, int episodeCount) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(episodeCount: Value(episodeCount)));
}
Future<void> updateSyncRuleFilter(String globalKey, String downloadFilter) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(downloadFilter: Value(downloadFilter)));
}
Future<void> updateSyncRuleEnabled(String globalKey, bool enabled) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(enabled: Value(enabled)));
}
Future<void> updateSyncRuleLastExecuted(String globalKey) async {
await (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
SyncRulesCompanion(lastExecutedAt: Value(DateTime.now().millisecondsSinceEpoch)),
);
}
Future<void> deleteSyncRule(String globalKey) async {
await (delete(syncRules)..where((t) => t.globalKey.equals(globalKey))).go();
}
// ============================================================
// Downloaded Media Queries for Watch State Sync
// ============================================================
@@ -225,6 +316,12 @@ LazyDatabase _openConnection() {
}
}
return NativeDatabase(file);
return NativeDatabase.createInBackground(
file,
setup: (db) {
db.execute('PRAGMA journal_mode=WAL');
db.execute('PRAGMA synchronous=NORMAL');
},
);
});
}
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
import 'package:drift/drift.dart';
import 'app_database.dart';
import '../models/download_models.dart';
/// Extension methods on AppDatabase for download operations
extension DownloadDatabaseOperations on AppDatabase {
/// Insert a new download into the database
Future<void> insertDownload({
required String serverId,
required String ratingKey,
required String globalKey,
required String type,
String? parentRatingKey,
String? grandparentRatingKey,
required int status,
int mediaIndex = 0,
}) async {
await into(downloadedMedia).insert(
DownloadedMediaCompanion.insert(
serverId: serverId,
ratingKey: ratingKey,
globalKey: globalKey,
type: type,
parentRatingKey: Value(parentRatingKey),
grandparentRatingKey: Value(grandparentRatingKey),
status: status,
mediaIndex: Value(mediaIndex),
),
mode: InsertMode.insertOrReplace,
);
}
/// Add item to download queue
Future<void> addToQueue({
required String mediaGlobalKey,
int priority = 0,
bool downloadSubtitles = true,
bool downloadArtwork = true,
}) async {
await into(downloadQueue).insert(
DownloadQueueCompanion.insert(
mediaGlobalKey: mediaGlobalKey,
priority: Value(priority),
addedAt: DateTime.now().millisecondsSinceEpoch,
downloadSubtitles: Value(downloadSubtitles),
downloadArtwork: Value(downloadArtwork),
),
mode: InsertMode.insertOrReplace,
);
}
/// Get next item from queue (highest priority, oldest first)
/// Only returns items that are not paused
Future<DownloadQueueItem?> getNextQueueItem() async {
// Join with downloadedMedia to check status and filter out paused items
final query = select(
downloadQueue,
).join([innerJoin(downloadedMedia, downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey))]);
query
..where(downloadedMedia.status.equals(DownloadStatus.queued.index))
..orderBy([
OrderingTerm(expression: downloadQueue.priority, mode: OrderingMode.desc),
OrderingTerm(expression: downloadQueue.addedAt),
])
..limit(1);
final result = await query.getSingleOrNull();
return result?.readTable(downloadQueue);
}
/// Update download status
Future<void> updateDownloadStatus(String globalKey, int status) async {
await (update(
downloadedMedia,
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(status: Value(status)));
}
/// Update download progress
Future<void> updateDownloadProgress(String globalKey, int progress, int downloadedBytes, int totalBytes) async {
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
DownloadedMediaCompanion(
progress: Value(progress),
downloadedBytes: Value(downloadedBytes),
totalBytes: Value(totalBytes),
),
);
}
/// Update video file path
Future<void> updateVideoFilePath(String globalKey, String filePath) async {
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
DownloadedMediaCompanion(
videoFilePath: Value(filePath),
downloadedAt: Value(DateTime.now().millisecondsSinceEpoch),
),
);
}
/// Update artwork paths
Future<void> updateArtworkPaths({required String globalKey, String? thumbPath}) async {
await (update(
downloadedMedia,
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(thumbPath: Value(thumbPath)));
}
/// Update download error and increment retry count
Future<void> updateDownloadError(String globalKey, String errorMessage) async {
// Get current retry count to increment it
final existing = await getDownloadedMedia(globalKey);
final currentCount = existing?.retryCount ?? 0;
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
DownloadedMediaCompanion(errorMessage: Value(errorMessage), retryCount: Value(currentCount + 1)),
);
}
/// Clear download error and reset retry count (for retry)
Future<void> clearDownloadError(String globalKey) async {
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
const DownloadedMediaCompanion(errorMessage: Value(null), retryCount: Value(0)),
);
}
/// Remove item from queue
Future<void> removeFromQueue(String mediaGlobalKey) async {
await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(mediaGlobalKey))).go();
}
/// Get downloaded media item
Future<DownloadedMediaItem?> getDownloadedMedia(String globalKey) {
return (select(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull();
}
/// Delete a download
Future<void> deleteDownload(String globalKey) async {
await (delete(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).go();
await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(globalKey))).go();
}
/// Get all downloaded episodes for a season
Future<List<DownloadedMediaItem>> getEpisodesBySeason(String seasonKey) {
return (select(downloadedMedia)..where((t) => t.parentRatingKey.equals(seasonKey))).get();
}
/// Get all downloaded episodes for a show
Future<List<DownloadedMediaItem>> getEpisodesByShow(String showKey) {
return (select(downloadedMedia)..where((t) => t.grandparentRatingKey.equals(showKey))).get();
}
/// Get all downloaded items for a specific server
Future<List<DownloadedMediaItem>> getDownloadsByServerId(String serverId) {
return (select(downloadedMedia)..where((t) => t.serverId.equals(serverId))).get();
}
/// Update the background_downloader task ID for a download
Future<void> updateBgTaskId(String globalKey, String? taskId) async {
await (update(
downloadedMedia,
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(bgTaskId: Value(taskId)));
}
/// Get the background_downloader task ID for a download
Future<String?> getBgTaskId(String globalKey) async {
final item = await getDownloadedMedia(globalKey);
return item?.bgTaskId;
}
}
+20
View File
@@ -48,6 +48,26 @@ class DownloadedMedia extends Table {
TextColumn get errorMessage => text().nullable()();
IntColumn get retryCount => integer().withDefault(const Constant(0))();
TextColumn get bgTaskId => text().nullable()();
IntColumn get mediaIndex => integer().withDefault(const Constant(0))();
}
/// Persistent sync rules for auto-downloading unwatched episodes.
///
/// Each rule keeps a rolling window of N unwatched episodes for a show/season,
/// or mirrors the current contents of a collection/playlist.
@DataClassName('SyncRuleItem')
class SyncRules extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get serverId => text()();
TextColumn get ratingKey => text()();
TextColumn get globalKey => text().unique()();
TextColumn get targetType => text()(); // 'show', 'season', 'collection', 'playlist'
IntColumn get episodeCount => integer()();
BoolColumn get enabled => boolean().withDefault(const Constant(true))();
IntColumn get createdAt => integer()();
IntColumn get lastExecutedAt => integer().nullable()();
IntColumn get mediaIndex => integer().withDefault(const Constant(0))();
TextColumn get downloadFilter => text().withDefault(const Constant('unwatched'))();
}
/// Queue for offline watch progress and manual watch actions.
+30 -28
View File
@@ -63,36 +63,47 @@ extension DpadKeyExtension on LogicalKeyboardKey {
bool get isDownKey => this == LogicalKeyboardKey.arrowDown;
}
/// Global helper to suppress the next SELECT key-up event.
class SelectKeyUpSuppressor {
static bool _suppressSelectUntilKeyUp = false;
/// Base class for suppressing key-up events after a key category triggers an
/// action (e.g. opening a sheet). While suppressed, all events for the matched
/// key category are consumed; suppression auto-clears on [KeyUpEvent].
class _KeyUpSuppressor {
final bool Function(LogicalKeyboardKey) _keyMatcher;
static void suppressSelectUntilKeyUp() {
_suppressSelectUntilKeyUp = true;
}
_KeyUpSuppressor(this._keyMatcher);
static void clearSuppression() {
_suppressSelectUntilKeyUp = false;
}
bool _suppressed = false;
static bool consumeIfSuppressed(KeyEvent event) {
if (!_suppressSelectUntilKeyUp) return false;
if (event.logicalKey.isSelectKey) {
if (event is KeyUpEvent) {
_suppressSelectUntilKeyUp = false;
}
void suppress() => _suppressed = true;
void clearSuppression() => _suppressed = false;
/// Returns `true` (consumed) when the event belongs to the matched key
/// category and suppression is active. Clears suppression on [KeyUpEvent].
bool consumeIfSuppressed(KeyEvent event) {
if (!_suppressed) return false;
if (_keyMatcher(event.logicalKey)) {
if (event is KeyUpEvent) _suppressed = false;
return true;
}
return false;
}
}
/// Global helper to suppress the next SELECT key-up event.
class SelectKeyUpSuppressor {
static final _instance = _KeyUpSuppressor((k) => k.isSelectKey);
static void suppressSelectUntilKeyUp() => _instance.suppress();
static void clearSuppression() => _instance.clearSuppression();
static bool consumeIfSuppressed(KeyEvent event) => _instance.consumeIfSuppressed(event);
}
/// Global helper to suppress the next BACK key-up event.
///
/// Use this when a modal (bottom sheet, dialog) closes to prevent
/// the BACK key-up from propagating to the underlying screen.
class BackKeyUpSuppressor {
static bool _suppressBackUntilKeyUp = false;
static final _instance = _KeyUpSuppressor((k) => k.isBackKey);
static bool _closedViaBackKey = false;
/// Mark that a modal is being closed via back key press.
@@ -109,26 +120,17 @@ class BackKeyUpSuppressor {
_closedViaBackKey = false;
return;
}
_suppressBackUntilKeyUp = true;
_instance.suppress();
}
/// Clear any pending suppression. Call when opening a new modal
/// to ensure stale suppression from previous closes doesn't affect it.
static void clearSuppression() {
_suppressBackUntilKeyUp = false;
_instance.clearSuppression();
_closedViaBackKey = false;
}
static bool consumeIfSuppressed(KeyEvent event) {
if (!_suppressBackUntilKeyUp) return false;
if (event.logicalKey.isBackKey) {
if (event is KeyUpEvent) {
_suppressBackUntilKeyUp = false;
}
return true;
}
return false;
}
static bool consumeIfSuppressed(KeyEvent event) => _instance.consumeIfSuppressed(event);
}
/// Tracks whether a back key is currently physically pressed.
+14 -10
View File
@@ -13,6 +13,9 @@ class FocusableAction {
/// Icon color. Ignored when [child] is provided.
final Color? iconColor;
/// Icon fill weight (0.01.0). Defaults to 1.0. Ignored when [child] is provided.
final double iconFill;
final String? tooltip;
final VoidCallback? onPressed;
@@ -23,6 +26,7 @@ class FocusableAction {
const FocusableAction({
this.icon = Icons.circle,
this.iconColor,
this.iconFill = 1.0,
this.tooltip,
this.onPressed,
this.child,
@@ -84,8 +88,11 @@ class FocusableActionBarState extends State<FocusableActionBar> {
late List<FocusNode> _focusNodes;
late List<bool> _focusStates;
/// Access a focus node by index (e.g. for external `requestFocus()` calls).
FocusNode getFocusNode(int index) => _focusNodes[index];
FocusNode? getFocusNode(int index) => index >= 0 && index < _focusNodes.length ? _focusNodes[index] : null;
void requestFocusOnFirst() {
if (_focusNodes.isNotEmpty) _focusNodes.first.requestFocus();
}
@override
void initState() {
@@ -135,9 +142,7 @@ class FocusableActionBarState extends State<FocusableActionBar> {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < widget.actions.length; i++) _buildButton(i, isKeyboard, duration),
],
children: [for (var i = 0; i < widget.actions.length; i++) _buildButton(i, isKeyboard, duration)],
);
}
@@ -156,9 +161,7 @@ class FocusableActionBarState extends State<FocusableActionBar> {
}
return dpadKeyHandler(
onSelect: action.onPressed,
onLeft: index > 0
? () => _focusNodes[index - 1].requestFocus()
: widget.onNavigateLeft,
onLeft: index > 0 ? () => _focusNodes[index - 1].requestFocus() : widget.onNavigateLeft,
onRight: index < _focusNodes.length - 1
? () => _focusNodes[index + 1].requestFocus()
: widget.onNavigateRight,
@@ -171,9 +174,10 @@ class FocusableActionBarState extends State<FocusableActionBar> {
duration: duration,
child: Container(
decoration: FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: 20),
child: action.child ??
child:
action.child ??
IconButton(
icon: AppIcon(action.icon, fill: 1, color: action.iconColor),
icon: AppIcon(action.icon, fill: action.iconFill, color: action.iconColor),
tooltip: action.tooltip,
onPressed: action.onPressed,
),
+1 -5
View File
@@ -85,11 +85,7 @@ class _FocusableButtonState extends State<FocusableButton> {
onNavigateLeft: widget.onNavigateLeft,
onNavigateRight: widget.onNavigateRight,
onBack: widget.onBack,
child: AnimatedOpacity(
opacity: showFocus ? 1.0 : opacity,
duration: duration,
child: widget.child,
),
child: AnimatedOpacity(opacity: showFocus ? 1.0 : opacity, duration: duration, child: widget.child),
);
}
}
+53 -7
View File
@@ -1,13 +1,19 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dpad_navigator.dart';
import 'key_event_utils.dart';
/// Callbacks for chip key event handling.
class ChipKeyCallbacks {
/// Called when SELECT key is pressed.
/// Called when SELECT key is pressed (short press when [onLongPress] is set).
final VoidCallback? onSelect;
/// Called when SELECT key is held for 500ms.
final VoidCallback? onLongPress;
/// Called when DOWN arrow is pressed.
final VoidCallback? onNavigateDown;
@@ -25,6 +31,7 @@ class ChipKeyCallbacks {
const ChipKeyCallbacks({
this.onSelect,
this.onLongPress,
this.onNavigateDown,
this.onNavigateUp,
this.onNavigateLeft,
@@ -53,6 +60,8 @@ class ChipKeyCallbacks {
mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
FocusNode? _internalFocusNode;
bool _isFocused = false;
Timer? _longPressTimer;
bool _isSelectKeyDown = false;
/// Override to return the widget's optional external focus node.
FocusNode? get widgetFocusNode;
@@ -85,6 +94,7 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
void disposeFocusNode() {
focusNode.removeListener(_onFocusChange);
_internalFocusNode?.dispose();
_longPressTimer?.cancel();
}
void _onFocusChange() {
@@ -96,7 +106,7 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
/// Shared key event handler for chip widgets.
///
/// Handles common key patterns:
/// - SELECT key -> onSelect
/// - SELECT key -> onSelect (short press) / onLongPress (hold 500ms)
/// - Arrow keys -> navigation callbacks
/// - BACK key -> onBack
///
@@ -112,16 +122,52 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
}
}
if (!event.isActionable) {
return KeyEventResult.ignored;
if (SelectKeyUpSuppressor.consumeIfSuppressed(event)) {
return KeyEventResult.handled;
}
// SELECT key activates the chip
if (key.isSelectKey && callbacks.onSelect != null) {
callbacks.onSelect!();
// SELECT key with long press support
if (key.isSelectKey) {
if (callbacks.onLongPress != null) {
if (event is KeyDownEvent) {
if (!_isSelectKeyDown) {
_isSelectKeyDown = true;
_longPressTimer?.cancel();
_longPressTimer = Timer(const Duration(milliseconds: 500), () {
if (mounted) {
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
callbacks.onLongPress?.call();
}
});
}
return KeyEventResult.handled;
} else if (event is KeyRepeatEvent) {
return KeyEventResult.handled;
} else if (event is KeyUpEvent) {
final timerWasActive = _longPressTimer?.isActive ?? false;
_longPressTimer?.cancel();
if (timerWasActive && _isSelectKeyDown) {
callbacks.onSelect?.call();
}
_isSelectKeyDown = false;
return KeyEventResult.handled;
}
} else if (callbacks.onSelect != null) {
return handleOneShotSelect(event, callbacks.onSelect!);
}
}
// Context menu key triggers long press directly
if (event.isActionable && key.isContextMenuKey && callbacks.onLongPress != null) {
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
callbacks.onLongPress!();
return KeyEventResult.handled;
}
if (!event.isActionable) {
return KeyEventResult.ignored;
}
// LEFT arrow - call callback if provided, otherwise propagate to parent
if (key.isLeftKey) {
if (callbacks.onNavigateLeft != null) {
+86
View File
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'dpad_navigator.dart';
import 'focusable_wrapper.dart';
import 'input_mode_tracker.dart';
/// A D-pad friendly slider that uses LEFT/RIGHT to adjust value
/// and lets UP/DOWN pass through for focus traversal.
///
/// Hides the thumb in keyboard mode when not focused, matching
/// the timeline slider pattern.
class FocusableSlider extends StatefulWidget {
final double value;
final double min;
final double max;
final int? divisions;
final ValueChanged<double>? onChanged;
final FocusNode? focusNode;
final bool autofocus;
const FocusableSlider({
super.key,
required this.value,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.onChanged,
this.focusNode,
this.autofocus = false,
});
@override
State<FocusableSlider> createState() => _FocusableSliderState();
}
class _FocusableSliderState extends State<FocusableSlider> {
bool _isFocused = false;
double get _step {
if (widget.divisions != null && widget.divisions! > 0) {
return (widget.max - widget.min) / widget.divisions!;
}
return (widget.max - widget.min) * 0.05;
}
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
final key = event.logicalKey;
if (key.isLeftKey || key.isRightKey) {
if (event.isActionable && widget.onChanged != null) {
final delta = key.isRightKey ? _step : -_step;
final newValue = (widget.value + delta).clamp(widget.min, widget.max);
widget.onChanged!(newValue);
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
@override
Widget build(BuildContext context) {
return FocusableWrapper(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
descendantsAreFocusable: false,
disableScale: true,
focusColor: Colors.transparent,
onFocusChange: (focused) => setState(() => _isFocused = focused),
onKeyEvent: _handleKeyEvent,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
thumbSize: WidgetStatePropertyAll(
(!InputModeTracker.isKeyboardMode(context) || _isFocused) ? const Size(4, 20) : Size.zero,
),
),
child: Slider(
value: widget.value,
min: widget.min,
max: widget.max,
divisions: widget.divisions,
onChanged: widget.onChanged,
),
),
);
}
}
+20 -8
View File
@@ -240,7 +240,17 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
final renderObject = context.findRenderObject();
if (renderObject == null) return;
final scrollable = Scrollable.maybeOf(context);
// Find the nearest vertical scrollable that actually has scroll range.
// Skip inner scrollables with no extent (e.g. shrinkWrap ListView
// with NeverScrollableScrollPhysics inside an outer scroll view)
// and horizontal scrollables (e.g. TabBarView) since we only do
// vertical scroll calculations.
var scrollable = Scrollable.maybeOf(context);
while (scrollable != null) {
final pos = scrollable.position;
if (pos.axis == Axis.vertical && pos.maxScrollExtent > pos.minScrollExtent) break;
scrollable = Scrollable.maybeOf(scrollable.context);
}
if (scrollable == null) return;
final viewport = scrollable.context.findRenderObject() as RenderBox?;
@@ -296,6 +306,7 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
scrollDelta -= (_focusDecorationPadding - projectedItemTop);
}
if (!position.maxScrollExtent.isFinite) return;
final targetOffset = (currentOffset + scrollDelta).clamp(position.minScrollExtent, position.maxScrollExtent);
position.animateTo(targetOffset, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
@@ -355,12 +366,8 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
_isSelectKeyDown = false;
return KeyEventResult.handled;
}
} else {
// Simple select handling without long-press
if (event is KeyDownEvent) {
widget.onSelect?.call();
return KeyEventResult.handled;
}
} else if (widget.onSelect != null) {
return handleOneShotSelect(event, widget.onSelect!);
}
}
@@ -419,7 +426,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
// Choose decoration based on useBackgroundFocus
final decoration = widget.useBackgroundFocus
? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius)
: FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: widget.borderRadius, color: widget.focusColor);
: FocusTheme.focusDecoration(
context,
isFocused: showFocus,
borderRadius: widget.borderRadius,
color: widget.focusColor,
);
Widget result = Focus(
focusNode: _focusNode,
+30 -5
View File
@@ -78,11 +78,32 @@ KeyEventResult handleBackKeyAction(KeyEvent event, VoidCallback onBack) {
}
KeyEventResult handleBackKeyNavigation<T>(BuildContext context, KeyEvent event, {T? result}) {
// Let system handle back if there's nothing to pop (e.g. root screen)
if (!Navigator.canPop(context)) {
return KeyEventResult.ignored;
}
// Don't handle back when a dialog/overlay is on top of our route —
// the overlay handles its own dismissal via DismissAction.
if (ModalRoute.of(context)?.isCurrent != true) {
return KeyEventResult.ignored;
}
// Handle on KeyUpEvent to prevent double-pop when returning from child screens
// (KeyDownEvent can be received by both the popping screen and the returned-to screen)
return handleBackKeyAction(event, () => Navigator.pop(context, result));
}
/// Handles a select key as a one-shot button activation.
///
/// Fires [onActivate] on the initial [KeyDownEvent] only.
/// Consumes all select key events (down, repeat, up) to prevent
/// unhandled events from reaching platform-level handling.
/// Returns [KeyEventResult.ignored] for non-select keys.
KeyEventResult handleOneShotSelect(KeyEvent event, VoidCallback onActivate) {
if (!event.logicalKey.isSelectKey) return KeyEventResult.ignored;
if (event is KeyDownEvent) onActivate();
return KeyEventResult.handled;
}
/// Creates a [FocusOnKeyEventCallback] that dispatches d-pad / arrow keys to
/// the provided directional callbacks.
///
@@ -90,7 +111,8 @@ KeyEventResult handleBackKeyNavigation<T>(BuildContext context, KeyEvent event,
/// (passed through to the framework). Directions mapped to a callback
/// automatically return [KeyEventResult.handled].
///
/// Only [KeyDownEvent] and [KeyRepeatEvent] are handled (via [isActionable]).
/// Directional keys repeat on [KeyRepeatEvent] (via [isActionable]).
/// Select is one-shot: fires on [KeyDownEvent] only, consumes repeat and up.
///
/// ```dart
/// Focus(
@@ -111,6 +133,13 @@ FocusOnKeyEventCallback dpadKeyHandler({
VoidCallback? onSelect,
}) {
return (FocusNode _, KeyEvent event) {
// Select: one-shot activation (no repeat), must run before isActionable
// filter so KeyUpEvent is also consumed.
if (onSelect != null) {
final result = handleOneShotSelect(event, onSelect);
if (result != KeyEventResult.ignored) return result;
}
if (!event.isActionable) return KeyEventResult.ignored;
final key = event.logicalKey;
@@ -130,10 +159,6 @@ FocusOnKeyEventCallback dpadKeyHandler({
onRight();
return KeyEventResult.handled;
}
if (key.isSelectKey && onSelect != null) {
onSelect();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Log ind med Plex",
"showQRCode": "Vis QR-kode",
"authenticate": "Godkend",
"authenticationTimeout": "Godkendelse fik timeout. Prøv igen.",
"scanQRToSignIn": "Scan denne QR-kode for at logge ind",
"waitingForAuth": "Venter på godkendelse...\nFærdiggør login i din browser.",
"useBrowser": "Brug browser"
},
"common": {
"cancel": "Annuller",
"save": "Gem",
"close": "Luk",
"clear": "Ryd",
"reset": "Nulstil",
"later": "Senere",
"submit": "Indsend",
"confirm": "Bekræft",
"retry": "Prøv igen",
"logout": "Log ud",
"unknown": "Ukendt",
"refresh": "Opdater",
"yes": "Ja",
"no": "Nej",
"delete": "Slet",
"shuffle": "Bland",
"addTo": "Tilføj til...",
"createNew": "Opret ny",
"connect": "Forbind",
"disconnect": "Afbryd",
"play": "Afspil",
"pause": "Pause",
"resume": "Genoptag",
"error": "Fejl",
"search": "Søg",
"home": "Hjem",
"back": "Tilbage",
"settings": "Indstillinger",
"mute": "Lydløs",
"ok": "OK",
"reconnect": "Genopret forbindelse",
"exitConfirmTitle": "Luk app?",
"exitConfirmMessage": "Er du sikker på, at du vil afslutte?",
"dontAskAgain": "Spørg ikke igen",
"exit": "Afslut",
"viewAll": "Vis alle",
"checkingNetwork": "Tjekker netværk...",
"refreshingServers": "Opdaterer servere...",
"loadingServers": "Indlæser servere...",
"connectingToServers": "Forbinder til servere...",
"startingOfflineMode": "Starter offlinetilstand...",
"loading": "Indlæser..."
},
"screens": {
"licenses": "Licenser",
"switchProfile": "Skift profil",
"subtitleStyling": "Undertekststil",
"mpvConfig": "mpv.conf",
"logs": "Logs"
},
"update": {
"available": "Opdatering tilgængelig",
"versionAvailable": "Version ${version} er tilgængelig",
"currentVersion": "Nuværende: ${version}",
"skipVersion": "Spring denne version over",
"viewRelease": "Vis udgivelse",
"latestVersion": "Du har den nyeste version",
"checkFailed": "Kunne ikke søge efter opdateringer"
},
"settings": {
"title": "Indstillinger",
"supportDeveloper": "Støt Plezy",
"supportDeveloperDescription": "Doner via Liberapay for at finansiere udviklingen",
"language": "Sprog",
"theme": "Tema",
"appearance": "Udseende",
"videoPlayback": "Videoafspilning",
"videoPlaybackDescription": "Konfigurer afspilningsadfærd",
"advanced": "Avanceret",
"episodePosterMode": "Episodeplakatstil",
"seriesPoster": "Serieplakat",
"seasonPoster": "Sæsonplakat",
"episodeThumbnail": "Miniature",
"showHeroSectionDescription": "Vis karrusel med udvalgt indhold på startskærmen",
"secondsLabel": "Sekunder",
"minutesLabel": "Minutter",
"secondsShort": "s",
"minutesShort": "m",
"durationHint": "Indtast varighed (${min}-${max})",
"systemTheme": "System",
"lightTheme": "Lys",
"darkTheme": "Mørk",
"oledTheme": "OLED",
"libraryDensity": "Bibliotekstæthed",
"compact": "Kompakt",
"comfortable": "Komfortabel",
"viewMode": "Visningstilstand",
"gridView": "Gitter",
"listView": "Liste",
"showHeroSection": "Vis hero-sektion",
"useGlobalHubs": "Brug Plex Home-layout",
"useGlobalHubsDescription": "Vis startsidehubbe som den officielle Plex-klient. Når slået fra, vises anbefalinger per bibliotek.",
"showServerNameOnHubs": "Vis servernavn på hubbe",
"showServerNameOnHubsDescription": "Vis altid servernavnet i hubtitler. Når slået fra, vises kun ved duplikerede navne.",
"alwaysKeepSidebarOpen": "Hold altid sidepanelet åbent",
"alwaysKeepSidebarOpenDescription": "Sidepanelet forbliver udvidet, og indholdsområdet tilpasser sig",
"showUnwatchedCount": "Vis antal usete",
"showUnwatchedCountDescription": "Vis antal usete episoder på serier og sæsoner",
"hideSpoilers": "Skjul spoilere for usete episoder",
"hideSpoilersDescription": "Slør miniaturebilleder og skjul beskrivelser for episoder, du ikke har set endnu",
"playerBackend": "Afspillerbackend",
"exoPlayer": "ExoPlayer (Anbefalet)",
"mpv": "mpv",
"hardwareDecoding": "Hardwaredekodning",
"hardwareDecodingDescription": "Brug hardwareacceleration når tilgængelig",
"bufferSize": "Bufferstørrelse",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Auto (Anbefalet)",
"bufferSizeWarning": "Din enhed har ${heap}MB hukommelse. En buffer på ${size}MB kan forårsage afspilningsproblemer.",
"subtitleStyling": "Undertekststil",
"subtitleStylingDescription": "Tilpas underteksters udseende",
"smallSkipDuration": "Kort spring-varighed",
"largeSkipDuration": "Lang spring-varighed",
"rewindOnResume": "Spol tilbage ved genoptagelse",
"secondsUnit": "${seconds} sekunder",
"defaultSleepTimer": "Standard sove-timer",
"minutesUnit": "${minutes} minutter",
"rememberTrackSelections": "Husk sporvalg per serie/film",
"rememberTrackSelectionsDescription": "Gem automatisk lyd- og undertekstsprogpræferencer når du skifter spor under afspilning",
"clickVideoTogglesPlayback": "Klik på video skifter afspil/pause",
"clickVideoTogglesPlaybackDescription": "Når aktiveret, afspiller/pauser klik på videoen. Ellers vises/skjules betjeningselementer.",
"videoPlayerControls": "Videoafspillerkontroller",
"keyboardShortcuts": "Tastaturgenveje",
"keyboardShortcutsDescription": "Tilpas tastaturgenveje",
"videoPlayerNavigation": "Videoafspillernavigation",
"videoPlayerNavigationDescription": "Brug piletaster til at navigere videoafspillerkontroller",
"watchTogetherRelay": "Watch Together-relay",
"watchTogetherRelayDescription": "Angiv en brugerdefineret relay-server til Watch Together. Alle deltagere skal bruge den samme server.",
"watchTogetherRelayHint": "https://min-relay.eksempel.dk",
"crashReporting": "Fejlrapportering",
"crashReportingDescription": "Send fejlrapporter for at hjælpe med at forbedre appen",
"debugLogging": "Fejlfindingslogning",
"debugLoggingDescription": "Aktiver detaljeret logning til fejlfinding",
"viewLogs": "Vis logs",
"viewLogsDescription": "Vis applikationslogs",
"clearCache": "Ryd cache",
"clearCacheDescription": "Dette rydder alle cachelagrede billeder og data. Appen kan tage længere tid om at indlæse indhold efter rydning.",
"clearCacheSuccess": "Cache ryddet",
"resetSettings": "Nulstil indstillinger",
"resetSettingsDescription": "Alle indstillinger nulstilles til standardværdier. Denne handling kan ikke fortrydes.",
"resetSettingsSuccess": "Indstillinger nulstillet",
"shortcutsReset": "Genveje nulstillet til standard",
"about": "Om",
"aboutDescription": "App-information og licenser",
"updates": "Opdateringer",
"updateAvailable": "Opdatering tilgængelig",
"checkForUpdates": "Søg efter opdateringer",
"validationErrorEnterNumber": "Indtast et gyldigt tal",
"validationErrorDuration": "Varighed skal være mellem ${min} og ${max} ${unit}",
"shortcutAlreadyAssigned": "Genvej allerede tildelt til ${action}",
"shortcutUpdated": "Genvej opdateret for ${action}",
"autoSkip": "Auto-spring",
"autoSkipIntro": "Auto-spring intro",
"autoSkipIntroDescription": "Spring automatisk intromarkører over efter få sekunder",
"autoSkipCredits": "Auto-spring rulletekster",
"autoSkipCreditsDescription": "Spring automatisk rulletekster over og afspil næste episode",
"autoSkipDelay": "Auto-spring forsinkelse",
"autoSkipDelayDescription": "Vent ${seconds} sekunder før automatisk spring",
"introPattern": "Intromarkørmønster",
"introPatternDescription": "Regulært udtryk til at genkende intromarkører i kapiteltitler",
"creditsPattern": "Rulletekstmarkørmønster",
"creditsPatternDescription": "Regulært udtryk til at genkende rulletekstmarkører i kapiteltitler",
"invalidRegex": "Ugyldigt regulært udtryk",
"downloads": "Downloads",
"downloadLocationDescription": "Vælg hvor downloadet indhold skal gemmes",
"downloadLocationDefault": "Standard (App-lagring)",
"downloadLocationCustom": "Brugerdefineret placering",
"selectFolder": "Vælg mappe",
"resetToDefault": "Nulstil til standard",
"currentPath": "Nuværende: ${path}",
"downloadLocationChanged": "Downloadplacering ændret",
"downloadLocationReset": "Downloadplacering nulstillet",
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
"downloadLocationSelectError": "Kunne ikke vælge mappe",
"downloadOnWifiOnly": "Download kun på WiFi",
"downloadOnWifiOnlyDescription": "Forhindre downloads på mobildata",
"autoRemoveWatchedDownloads": "Fjern sete downloads automatisk",
"autoRemoveWatchedDownloadsDescription": "Slet automatisk downloadede episoder og film, når de markeres som set",
"cellularDownloadBlocked": "Downloads er deaktiveret på mobildata. Opret forbindelse til WiFi eller ændr indstillingen.",
"maxVolume": "Maksimal lydstyrke",
"maxVolumeDescription": "Tillad lydstyrkeforstærkning over 100% for stille medier",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Vis hvad du ser på Discord",
"trakt": "Trakt",
"traktDescription": "Synkroniser visningshistorik med Trakt",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Tillad mobilenheder på dit netværk at styre denne app",
"autoPip": "Auto billede-i-billede",
"autoPipDescription": "Gå automatisk til billede-i-billede når du forlader appen under afspilning",
"matchContentFrameRate": "Match indholdets billedhastighed",
"matchContentFrameRateDescription": "Juster skærmens opdateringshastighed til videoindhold for at reducere hakken og spare batteri",
"matchRefreshRate": "Match opdateringshastighed",
"matchRefreshRateDescription": "Skift skærmens opdateringshastighed til at matche videoindhold i fuldskærm",
"matchDynamicRange": "Match dynamisk område",
"matchDynamicRangeDescription": "Aktiver automatisk HDR for HDR-indhold og gendan SDR når afspilleren lukkes",
"displaySwitchDelay": "Forsinkelse ved skærmskift",
"tunneledPlayback": "Tunneleret afspilning",
"tunneledPlaybackDescription": "Brug hardwareaccelereret videotunneling. Deaktiver hvis du ser sort skærm med lyd på HDR-indhold",
"requireProfileSelectionOnOpen": "Spørg om profil ved åbning",
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åbnes",
"confirmExitOnBack": "Bekræft før lukning",
"confirmExitOnBackDescription": "Vis bekræftelsesdialog ved tryk på tilbage for at lukke appen",
"forceTvMode": "Gennemtving TV-tilstand",
"forceTvModeDescription": "Brug TV-layoutet uanset automatisk registrering. Nyttigt på Android TV-enheder, der ikke rapporterer leanback-funktionen. Genstarter appen ved ændring.",
"autoHidePerformanceOverlay": "Skjul ydelses-overlay automatisk",
"autoHidePerformanceOverlayDescription": "Fade ydelses-overlayet med afspilningskontrollerne",
"showNavBarLabels": "Vis navigationsbarlabels",
"showNavBarLabelsDescription": "Vis tekstlabels under navigationsbarikoner",
"liveTvDefaultFavorites": "Standard til favoritkanaler",
"liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Søg film, serier, musik...",
"tryDifferentTerm": "Prøv en anden søgning",
"searchYourMedia": "Søg i dine medier",
"enterTitleActorOrKeyword": "Indtast titel, skuespiller eller nøgleord"
},
"hotkeys": {
"setShortcutFor": "Indstil genvej for ${actionName}",
"clearShortcut": "Ryd genvej",
"actions": {
"playPause": "Afspil/Pause",
"volumeUp": "Lydstyrke op",
"volumeDown": "Lydstyrke ned",
"seekForward": "Spol frem (${seconds}s)",
"seekBackward": "Spol tilbage (${seconds}s)",
"fullscreenToggle": "Skift fuldskærm",
"muteToggle": "Skift lydløs",
"subtitleToggle": "Skift undertekster",
"audioTrackNext": "Næste lydspor",
"subtitleTrackNext": "Næste undertekstspor",
"chapterNext": "Næste kapitel",
"chapterPrevious": "Forrige kapitel",
"speedIncrease": "Øg hastighed",
"speedDecrease": "Sænk hastighed",
"speedReset": "Nulstil hastighed",
"subSeekNext": "Søg til næste undertekst",
"subSeekPrev": "Søg til forrige undertekst",
"shaderToggle": "Skift shadere",
"skipMarker": "Spring intro/rulletekster over"
}
},
"fileInfo": {
"title": "Filinfo",
"video": "Video",
"audio": "Lyd",
"file": "Fil",
"advanced": "Avanceret",
"codec": "Codec",
"resolution": "Opløsning",
"bitrate": "Bitrate",
"frameRate": "Billedhastighed",
"aspectRatio": "Billedformat",
"profile": "Profil",
"bitDepth": "Bitdybde",
"colorSpace": "Farverum",
"colorRange": "Farveområde",
"colorPrimaries": "Farveprimærer",
"chromaSubsampling": "Chroma-subsampling",
"channels": "Kanaler",
"subtitles": "Undertekster",
"overallBitrate": "Samlet bitrate",
"path": "Sti",
"size": "Størrelse",
"container": "Container",
"duration": "Varighed",
"optimizedForStreaming": "Optimeret til streaming",
"has64bitOffsets": "64-bit offsets"
},
"mediaMenu": {
"markAsWatched": "Markér som set",
"markAsUnwatched": "Markér som uset",
"removeFromContinueWatching": "Fjern fra Fortsæt med at se",
"goToSeries": "Gå til serie",
"goToSeason": "Gå til sæson",
"shufflePlay": "Afspil tilfældigt",
"fileInfo": "Filinfo",
"deleteFromServer": "Slet fra server",
"confirmDelete": "Dette sletter permanent disse medier og filer fra din server. Dette kan ikke fortrydes.",
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
"mediaDeletedSuccessfully": "Medieelement slettet",
"mediaFailedToDelete": "Kunne ikke slette medieelement",
"rate": "Bedøm",
"playFromBeginning": "Afspil fra begyndelsen",
"playVersion": "Afspil version..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
"mediaCardShow": "${title}, TV-serie",
"mediaCardEpisode": "${title}, ${episodeInfo}",
"mediaCardSeason": "${title}, ${seasonInfo}",
"mediaCardWatched": "set",
"mediaCardPartiallyWatched": "${percent} procent set",
"mediaCardUnwatched": "uset",
"tapToPlay": "Tryk for at afspille"
},
"tooltips": {
"shufflePlay": "Afspil tilfældigt",
"playTrailer": "Afspil trailer",
"markAsWatched": "Markér som set",
"markAsUnwatched": "Markér som uset"
},
"videoControls": {
"audioLabel": "Lyd",
"subtitlesLabel": "Undertekster",
"resetToZero": "Nulstil til 0ms",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label} afspilles senere",
"playsEarlier": "${label} afspilles tidligere",
"noOffset": "Ingen forskydning",
"letterbox": "Letterbox",
"fillScreen": "Fyld skærm",
"stretch": "Stræk",
"lockRotation": "Lås rotation",
"unlockRotation": "Lås rotation op",
"timerActive": "Timer aktiv",
"playbackWillPauseIn": "Afspilning pauses om ${duration}",
"stillWatching": "Ser du stadig?",
"pausingIn": "Pauser om ${seconds}s",
"continueWatching": "Fortsæt",
"autoPlayNext": "Auto-afspil næste",
"playNext": "Afspil næste",
"playButton": "Afspil",
"pauseButton": "Pause",
"seekBackwardButton": "Spol ${seconds} sekunder tilbage",
"seekForwardButton": "Spol ${seconds} sekunder frem",
"previousButton": "Forrige episode",
"nextButton": "Næste episode",
"previousChapterButton": "Forrige kapitel",
"nextChapterButton": "Næste kapitel",
"muteButton": "Lydløs",
"unmuteButton": "Slå lyd til",
"settingsButton": "Videoindstillinger",
"tracksButton": "Lyd og undertekster",
"chaptersButton": "Kapitler",
"versionsButton": "Videoversioner",
"pipButton": "Billede-i-billede-tilstand",
"aspectRatioButton": "Billedformat",
"ambientLighting": "Omgivelsesbelysning",
"fullscreenButton": "Fuldskærm",
"exitFullscreenButton": "Forlad fuldskærm",
"alwaysOnTopButton": "Altid øverst",
"rotationLockButton": "Rotationslås",
"lockScreen": "Lås skærm",
"screenLockButton": "Skærmlås",
"longPressToUnlock": "Langt tryk for at låse op",
"timelineSlider": "Videotidslinje",
"volumeSlider": "Lydstyrkeniveau",
"endsAt": "Slutter kl. ${time}",
"pipActive": "Afspiller i billede-i-billede",
"pipFailed": "Billede-i-billede kunne ikke starte",
"pipErrors": {
"androidVersion": "Kræver Android 8.0 eller nyere",
"iosVersion": "Kræver iOS 15.0 eller nyere",
"permissionDisabled": "Billede-i-billede-tilladelse er deaktiveret. Aktivér i Indstillinger > Apps > Plezy > Billede-i-billede",
"notSupported": "Enheden understøtter ikke billede-i-billede",
"voSwitchFailed": "Kunne ikke skifte videooutput til billede-i-billede",
"failed": "Billede-i-billede kunne ikke starte",
"unknown": "Der opstod en fejl: ${error}"
},
"chapters": "Kapitler",
"noChaptersAvailable": "Ingen kapitler tilgængelige",
"queue": "Kø",
"noQueueItems": "Ingen elementer i køen",
"searchSubtitles": "Søg undertekster",
"language": "Sprog",
"noSubtitlesFound": "Ingen undertekster fundet",
"subtitleDownloaded": "Undertekst downloadet",
"subtitleDownloadFailed": "Kunne ikke downloade undertekst",
"searchLanguages": "Søg sprog..."
},
"userStatus": {
"admin": "Administrator",
"restricted": "Begrænset",
"protected": "Beskyttet",
"current": "NUVÆRENDE"
},
"messages": {
"markedAsWatched": "Markeret som set",
"markedAsUnwatched": "Markeret som uset",
"markedAsWatchedOffline": "Markeret som set (synkroniseres online)",
"markedAsUnwatchedOffline": "Markeret som uset (synkroniseres online)",
"autoRemovedWatchedDownload": "Automatisk fjernet: ${title}",
"removedFromContinueWatching": "Fjernet fra Fortsæt med at se",
"errorLoading": "Fejl: ${error}",
"fileInfoNotAvailable": "Filinfo ikke tilgængelig",
"errorLoadingFileInfo": "Fejl ved indlæsning af filinfo: ${error}",
"errorLoadingSeries": "Fejl ved indlæsning af serie",
"errorLoadingSeason": "Fejl ved indlæsning af sæson",
"musicNotSupported": "Musikafspilning understøttes endnu ikke",
"logsCleared": "Logs ryddet",
"logsCopied": "Logs kopieret til udklipsholder",
"noLogsAvailable": "Ingen logs tilgængelige",
"libraryScanning": "Scanner \"${title}\"...",
"libraryScanStarted": "Biblioteksscanning startet for \"${title}\"",
"libraryScanFailed": "Kunne ikke scanne bibliotek: ${error}",
"metadataRefreshing": "Opdaterer metadata for \"${title}\"...",
"metadataRefreshStarted": "Metadataopdatering startet for \"${title}\"",
"metadataRefreshFailed": "Kunne ikke opdatere metadata: ${error}",
"logoutConfirm": "Er du sikker på, at du vil logge ud?",
"noSeasonsFound": "Ingen sæsoner fundet",
"noEpisodesFound": "Ingen episoder fundet i første sæson",
"noEpisodesFoundGeneral": "Ingen episoder fundet",
"noResultsFound": "Ingen resultater fundet",
"sleepTimerSet": "Sove-timer indstillet til ${label}",
"noItemsAvailable": "Ingen elementer tilgængelige",
"failedToCreatePlayQueueNoItems": "Kunne ikke oprette afspilningskø — ingen elementer",
"failedPlayback": "Kunne ikke ${action}: ${error}",
"switchingToCompatiblePlayer": "Skifter til kompatibel afspiller...",
"serverLimitTitle": "Afspilning mislykkedes",
"serverLimitBody": "Serveren returnerede en fejl (HTTP 500). Det betyder som regel, at serverejeren har sat en båndbredde- eller transkodningsgrænse, der afviser din session. Der er intet at gøre fra klienten — serverejeren skal justere sine indstillinger.",
"logsUploaded": "Logs uploadet",
"logsUploadFailed": "Kunne ikke uploade logs",
"logId": "Log-ID"
},
"subtitlingStyling": {
"text": "Tekst",
"border": "Kant",
"background": "Baggrund",
"fontSize": "Skriftstørrelse",
"textColor": "Tekstfarve",
"borderSize": "Kantstørrelse",
"borderColor": "Kantfarve",
"backgroundOpacity": "Baggrundsgennemsigtighed",
"backgroundColor": "Baggrundsfarve",
"position": "Position",
"assOverride": "ASS-tilsidesættelse",
"bold": "Fed",
"italic": "Kursiv"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "Avancerede videoafspillerindstillinger",
"presets": "Forudindstillinger",
"noPresets": "Ingen gemte forudindstillinger",
"saveAsPreset": "Gem som forudindstilling...",
"presetName": "Forudindstillingsnavn",
"presetNameHint": "Indtast et navn for denne forudindstilling",
"loadPreset": "Indlæs",
"deletePreset": "Slet",
"presetSaved": "Forudindstilling gemt",
"presetLoaded": "Forudindstilling indlæst",
"presetDeleted": "Forudindstilling slettet",
"confirmDeletePreset": "Er du sikker på, at du vil slette denne forudindstilling?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Bekræft handling"
},
"discover": {
"title": "Opdag",
"switchProfile": "Skift profil",
"noContentAvailable": "Intet indhold tilgængeligt",
"addMediaToLibraries": "Tilføj medier til dine biblioteker",
"continueWatching": "Fortsæt med at se",
"playEpisode": "S${season}E${episode}",
"overview": "Oversigt",
"cast": "Rollebesætning",
"extras": "Trailere og ekstra",
"studio": "Studie",
"rating": "Bedømmelse",
"movie": "Film",
"tvShow": "TV-serie",
"minutesLeft": "${minutes} min tilbage"
},
"errors": {
"searchFailed": "Søgning mislykkedes: ${error}",
"connectionTimeout": "Forbindelsestimeout ved indlæsning af ${context}",
"connectionFailed": "Kunne ikke forbinde til Plex-server",
"failedToLoad": "Kunne ikke indlæse ${context}: ${error}",
"noClientAvailable": "Ingen klient tilgængelig",
"authenticationFailed": "Godkendelse mislykkedes: ${error}",
"couldNotLaunchUrl": "Kunne ikke åbne godkendelses-URL",
"pleaseEnterToken": "Indtast et token",
"invalidToken": "Ugyldigt token",
"failedToVerifyToken": "Kunne ikke verificere token: ${error}",
"failedToSwitchProfile": "Kunne ikke skifte til ${displayName}"
},
"libraries": {
"title": "Biblioteker",
"scanLibraryFiles": "Scan biblioteksfiler",
"scanLibrary": "Scan bibliotek",
"analyze": "Analysér",
"analyzeLibrary": "Analysér bibliotek",
"refreshMetadata": "Opdater metadata",
"emptyTrash": "Tøm papirkurv",
"emptyingTrash": "Tømmer papirkurv for \"${title}\"...",
"trashEmptied": "Papirkurv tømt for \"${title}\"",
"failedToEmptyTrash": "Kunne ikke tømme papirkurv: ${error}",
"analyzing": "Analyserer \"${title}\"...",
"analysisStarted": "Analyse startet for \"${title}\"",
"failedToAnalyze": "Kunne ikke analysere bibliotek: ${error}",
"noLibrariesFound": "Ingen biblioteker fundet",
"thisLibraryIsEmpty": "Dette bibliotek er tomt",
"all": "Alle",
"clearAll": "Ryd alle",
"scanLibraryConfirm": "Er du sikker på, at du vil scanne \"${title}\"?",
"analyzeLibraryConfirm": "Er du sikker på, at du vil analysere \"${title}\"?",
"refreshMetadataConfirm": "Er du sikker på, at du vil opdatere metadata for \"${title}\"?",
"emptyTrashConfirm": "Er du sikker på, at du vil tømme papirkurven for \"${title}\"?",
"manageLibraries": "Administrer biblioteker",
"sort": "Sortér",
"sortBy": "Sortér efter",
"filters": "Filtre",
"confirmActionMessage": "Er du sikker på, at du vil udføre denne handling?",
"showLibrary": "Vis bibliotek",
"hideLibrary": "Skjul bibliotek",
"libraryOptions": "Biblioteksindstillinger",
"content": "biblioteksindhold",
"selectLibrary": "Vælg bibliotek",
"filtersWithCount": "Filtre (${count})",
"noRecommendations": "Ingen anbefalinger tilgængelige",
"noCollections": "Ingen samlinger i dette bibliotek",
"noFoldersFound": "Ingen mapper fundet",
"folders": "mapper",
"tabs": {
"recommended": "Anbefalet",
"browse": "Gennemse",
"collections": "Samlinger",
"playlists": "Playlister"
},
"groupings": {
"title": "Gruppering",
"all": "Alle",
"movies": "Film",
"shows": "TV-serier",
"seasons": "Sæsoner",
"episodes": "Episoder",
"folders": "Mapper"
}
},
"about": {
"title": "Om",
"openSourceLicenses": "Open source-licenser",
"versionLabel": "Version ${version}",
"appDescription": "En smuk Plex-klient til Flutter",
"viewLicensesDescription": "Se licenser for tredjepartsbiblioteker"
},
"serverSelection": {
"allServerConnectionsFailed": "Kunne ikke forbinde til nogen servere. Tjek dit netværk og prøv igen.",
"noServersFoundForAccount": "Ingen servere fundet for ${username} (${email})",
"failedToLoadServers": "Kunne ikke indlæse servere: ${error}"
},
"hubDetail": {
"title": "Titel",
"releaseYear": "Udgivelsesår",
"dateAdded": "Tilføjelsesdato",
"rating": "Bedømmelse",
"noItemsFound": "Ingen elementer fundet"
},
"logs": {
"clearLogs": "Ryd logs",
"copyLogs": "Kopiér logs",
"uploadLogs": "Upload logs"
},
"licenses": {
"relatedPackages": "Relaterede pakker",
"license": "Licens",
"licenseNumber": "Licens ${number}",
"licensesCount": "${count} licenser"
},
"navigation": {
"libraries": "Biblioteker",
"downloads": "Downloads",
"liveTv": "Live TV"
},
"liveTv": {
"title": "Live TV",
"guide": "Guide",
"noChannels": "Ingen kanaler tilgængelige",
"noDvr": "Ingen DVR konfigureret på nogen server",
"noPrograms": "Ingen programdata tilgængelig",
"live": "LIVE",
"reloadGuide": "Genindlæs guide",
"now": "Nu",
"today": "I dag",
"midnight": "Midnat",
"overnight": "Nat",
"morning": "Morgen",
"daytime": "Dagtid",
"evening": "Aften",
"lateNight": "Sen aften",
"whatsOn": "Hvad der kører",
"watchChannel": "Se kanal",
"favorites": "Favoritter",
"reorderFavorites": "Omarranger favoritter",
"joinSession": "Deltag i igangværende session",
"watchFromStart": "Se fra start (${minutes} min siden)",
"watchLive": "Se live",
"goToLive": "Gå til live"
},
"collections": {
"title": "Samlinger",
"collection": "Samling",
"empty": "Samlingen er tom",
"unknownLibrarySection": "Kan ikke slette: Ukendt bibliotekssektion",
"deleteCollection": "Slet samling",
"deleteConfirm": "Er du sikker på, at du vil slette \"${title}\"? Denne handling kan ikke fortrydes.",
"deleted": "Samling slettet",
"deleteFailed": "Kunne ikke slette samling",
"deleteFailedWithError": "Kunne ikke slette samling: ${error}",
"failedToLoadItems": "Kunne ikke indlæse samlingselementer: ${error}",
"selectCollection": "Vælg samling",
"collectionName": "Samlingsnavn",
"enterCollectionName": "Indtast samlingsnavn",
"addedToCollection": "Tilføjet til samling",
"errorAddingToCollection": "Kunne ikke tilføje til samling",
"created": "Samling oprettet",
"removeFromCollection": "Fjern fra samling",
"removeFromCollectionConfirm": "Fjern \"${title}\" fra denne samling?",
"removedFromCollection": "Fjernet fra samling",
"removeFromCollectionFailed": "Kunne ikke fjerne fra samling",
"removeFromCollectionError": "Fejl ved fjernelse fra samling: ${error}",
"searchCollections": "Søg i samlinger..."
},
"playlists": {
"title": "Playlister",
"playlist": "Playliste",
"noPlaylists": "Ingen playlister fundet",
"create": "Opret playliste",
"playlistName": "Playlistenavn",
"enterPlaylistName": "Indtast playlistenavn",
"delete": "Slet playliste",
"removeItem": "Fjern fra playliste",
"smartPlaylist": "Smart playliste",
"itemCount": "${count} elementer",
"oneItem": "1 element",
"emptyPlaylist": "Denne playliste er tom",
"deleteConfirm": "Slet playliste?",
"deleteMessage": "Er du sikker på, at du vil slette \"${name}\"?",
"created": "Playliste oprettet",
"deleted": "Playliste slettet",
"itemAdded": "Tilføjet til playliste",
"itemRemoved": "Fjernet fra playliste",
"selectPlaylist": "Vælg playliste",
"errorCreating": "Kunne ikke oprette playliste",
"errorDeleting": "Kunne ikke slette playliste",
"errorLoading": "Kunne ikke indlæse playlister",
"errorAdding": "Kunne ikke tilføje til playliste",
"errorReordering": "Kunne ikke ændre rækkefølge på playlisteelement",
"errorRemoving": "Kunne ikke fjerne fra playliste"
},
"watchTogether": {
"title": "Se sammen",
"description": "Se indhold synkroniseret med venner og familie",
"createSession": "Opret session",
"creating": "Opretter...",
"joinSession": "Deltag i session",
"joining": "Deltager...",
"controlMode": "Kontroltilstand",
"controlModeQuestion": "Hvem kan styre afspilning?",
"hostOnly": "Kun vært",
"anyone": "Alle",
"hostingSession": "Vært for session",
"inSession": "I session",
"sessionCode": "Sessionskode",
"hostControlsPlayback": "Vært styrer afspilning",
"anyoneCanControl": "Alle kan styre afspilning",
"hostControls": "Værtskontrol",
"anyoneControls": "Alle styrer",
"participants": "Deltagere",
"host": "Vært",
"hostBadge": "HOST",
"youAreHost": "Du er vært",
"watchingWithOthers": "Ser med andre",
"endSession": "Afslut session",
"leaveSession": "Forlad session",
"endSessionQuestion": "Afslut session?",
"leaveSessionQuestion": "Forlad session?",
"endSessionConfirm": "Dette afslutter sessionen for alle deltagere.",
"leaveSessionConfirm": "Du vil blive fjernet fra sessionen.",
"endSessionConfirmOverlay": "Dette afslutter se-sessionen for alle deltagere.",
"leaveSessionConfirmOverlay": "Du vil blive afbrudt fra se-sessionen.",
"end": "Afslut",
"leave": "Forlad",
"syncing": "Synkroniserer...",
"joinWatchSession": "Deltag i se-session",
"enterCodeHint": "Indtast 5-tegns kode",
"pasteFromClipboard": "Indsæt fra udklipsholder",
"pleaseEnterCode": "Indtast en sessionskode",
"codeMustBe5Chars": "Sessionskode skal være 5 tegn",
"joinInstructions": "Indtast sessionskoden delt af værten for at deltage i se-sessionen.",
"failedToCreate": "Kunne ikke oprette session",
"failedToJoin": "Kunne ikke deltage i session",
"sessionCodeCopied": "Sessionskode kopieret til udklipsholder",
"relayUnreachable": "Relay-serveren kan ikke nås. Dette kan skyldes, at din udbyder blokerer forbindelsen. Du kan stadig prøve, men Se sammen virker muligvis ikke.",
"reconnectingToHost": "Genopretter forbindelse til vært...",
"currentPlayback": "Nuværende afspilning",
"joinCurrentPlayback": "Deltag i nuværende afspilning",
"joinCurrentPlaybackDescription": "Hop tilbage til det værten ser nu",
"failedToOpenCurrentPlayback": "Kunne ikke åbne nuværende afspilning",
"participantJoined": "${name} deltog",
"participantLeft": "${name} forlod",
"participantPaused": "${name} satte på pause",
"participantResumed": "${name} genoptog",
"participantSeeked": "${name} spoled",
"participantBuffering": "${name} bufferer",
"waitingForParticipants": "Venter på at andre indlæser...",
"recentRooms": "Seneste rum",
"renameRoom": "Omdøb rum",
"removeRoom": "Fjern"
},
"downloads": {
"title": "Downloads",
"manage": "Administrer",
"tvShows": "TV-serier",
"movies": "Film",
"noDownloads": "Ingen downloads endnu",
"noDownloadsDescription": "Downloadet indhold vises her til offlinevisning",
"downloadNow": "Download",
"deleteDownload": "Slet download",
"retryDownload": "Prøv download igen",
"downloadQueued": "Download i kø",
"serverErrorBitrate": "Serverfejl — filen overskrider muligvis grænsen for fjernstreaming-bitrate",
"episodesQueued": "${count} episoder i downloadkø",
"downloadDeleted": "Download slettet",
"deleteConfirm": "Er du sikker på, at du vil slette \"${title}\"? Den downloadede fil fjernes fra din enhed.",
"deletingWithProgress": "Sletter ${title}... (${current} af ${total})",
"noDownloadsTree": "Ingen downloads",
"pauseAll": "Pause alle",
"resumeAll": "Genoptag alle",
"deleteAll": "Slet alle",
"selectVersion": "Vælg version",
"allEpisodes": "Alle episoder",
"unwatchedOnly": "Kun usete",
"nextNUnwatched": "Næste ${count} usete",
"customAmount": "Angiv antal...",
"howManyEpisodes": "Hvor mange episoder?",
"itemsQueued": "${count} elementer sat i kø til download",
"keepSynced": "Hold synkroniseret",
"downloadOnce": "Download én gang",
"keepNUnwatched": "Behold ${count} usete",
"editSyncRule": "Rediger synkroniseringsregel",
"removeSyncRule": "Fjern synkroniseringsregel",
"removeSyncRuleConfirm": "Stop synkronisering af \"${title}\"? Downloadede episoder beholdes.",
"syncRuleCreated": "Synkroniseringsregel oprettet — beholder ${count} usete episoder",
"syncRuleUpdated": "Synkroniseringsregel opdateret",
"syncRuleRemoved": "Synkroniseringsregel fjernet",
"syncedNewEpisodes": "Synkroniserede ${count} nye episoder for ${title}",
"activeSyncRules": "Synkroniseringsregler",
"noSyncRules": "Ingen synkroniseringsregler",
"manageSyncRule": "Administrer synkronisering",
"editEpisodeCount": "Antal episoder",
"editSyncFilter": "Synkroniseringsfilter",
"syncAllItems": "Synkroniserer alle elementer",
"syncUnwatchedItems": "Synkroniserer usete elementer",
"syncRuleListCreated": "Synkroniseringsregel oprettet"
},
"shaders": {
"title": "Shadere",
"noShaderDescription": "Ingen videoforbedring",
"nvscalerDescription": "NVIDIA-billedskalering for skarpere video",
"qualityFast": "Hurtig",
"qualityHQ": "Høj kvalitet",
"mode": "Tilstand",
"importShader": "Importér shader",
"customShaderDescription": "Brugerdefineret GLSL-shader",
"shaderImported": "Shader importeret",
"shaderImportFailed": "Kunne ikke importere shader",
"deleteShader": "Slet shader",
"deleteShaderConfirm": "Slet \"${name}\"?"
},
"companionRemote": {
"title": "Fjernbetjening",
"connectedTo": "Forbundet til ${name}",
"session": {
"startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke starte fjernserver:",
"hostAddress": "Værtsadresse",
"connected": "Forbundet",
"serverRunning": "Fjernserver aktiv",
"serverStopped": "Fjernserver stoppet",
"serverRunningDescription": "Mobilenheder på dit netværk kan finde og oprette forbindelse til denne app",
"serverStoppedDescription": "Start serveren for at tillade mobilenheder at oprette forbindelse",
"usePhoneToControl": "Brug din mobilenhed til at styre denne app",
"startServer": "Start server",
"stopServer": "Stop server",
"minimize": "Minimér"
},
"pairing": {
"discoveryDescription": "Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "Opretter forbindelse...",
"searchingForDevices": "Søger efter enheder...",
"noDevicesFound": "Ingen enheder fundet på dit netværk",
"noDevicesHint": "Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk",
"availableDevices": "Tilgængelige enheder",
"manualConnection": "Manuel forbindelse",
"cryptoInitFailed": "Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.",
"validationHostRequired": "Angiv venligst værtsadresse",
"validationHostFormat": "Format skal være IP:port (f.eks. 192.168.1.100:48632)",
"connectionTimedOut": "Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.",
"sessionNotFound": "Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.",
"authFailed": "Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.",
"failedToConnect": "Kunne ikke oprette forbindelse: ${error}"
},
"remote": {
"disconnectConfirm": "Vil du afbryde fra fjernsessionen?",
"reconnecting": "Genopretter forbindelse...",
"attemptOf": "Forsøg ${current} af 5",
"retryNow": "Prøv igen nu",
"tabRemote": "Fjernbetjening",
"tabPlay": "Afspil",
"tabMore": "Mere",
"menu": "Menu",
"tabNavigation": "Fanenavigation",
"tabDiscover": "Opdag",
"tabLibraries": "Biblioteker",
"tabSearch": "Søg",
"tabDownloads": "Downloads",
"tabSettings": "Indstillinger",
"previous": "Forrige",
"playPause": "Afspil/Pause",
"next": "Næste",
"seekBack": "Spol tilbage",
"stop": "Stop",
"seekForward": "Spol frem",
"volume": "Lydstyrke",
"volumeDown": "Ned",
"volumeUp": "Op",
"fullscreen": "Fuldskærm",
"subtitles": "Undertekster",
"audio": "Lyd",
"searchHint": "Søg på desktop..."
}
},
"videoSettings": {
"playbackSettings": "Afspilningsindstillinger",
"playbackSpeed": "Afspilningshastighed",
"sleepTimer": "Sove-timer",
"audioSync": "Lydsynkronisering",
"subtitleSync": "Undertekstsynkronisering",
"hdr": "HDR",
"audioOutput": "Lydoutput",
"performanceOverlay": "Ydelsesoverlay",
"audioPassthrough": "Lyd-passthrough",
"audioNormalization": "Lydnormalisering"
},
"externalPlayer": {
"title": "Ekstern afspiller",
"useExternalPlayer": "Brug ekstern afspiller",
"useExternalPlayerDescription": "Åbn videoer i en ekstern app i stedet for den indbyggede afspiller",
"selectPlayer": "Vælg afspiller",
"customPlayers": "Brugerdefinerede afspillere",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Tilføj brugerdefineret afspiller",
"playerName": "Afspillernavn",
"playerCommand": "Kommando",
"playerPackage": "Pakkenavn",
"playerUrlScheme": "URL-skema",
"off": "Fra",
"launchFailed": "Kunne ikke åbne ekstern afspiller",
"appNotInstalled": "${name} er ikke installeret",
"playInExternalPlayer": "Afspil i ekstern afspiller"
},
"metadataEdit": {
"editMetadata": "Redigér...",
"screenTitle": "Redigér metadata",
"basicInfo": "Grundlæggende info",
"artwork": "Grafik",
"advancedSettings": "Avancerede indstillinger",
"title": "Titel",
"sortTitle": "Sorteringstitel",
"originalTitle": "Originaltitel",
"releaseDate": "Udgivelsesdato",
"contentRating": "Aldersgrænse",
"studio": "Studie",
"tagline": "Tagline",
"summary": "Resumé",
"poster": "Plakat",
"background": "Baggrund",
"logo": "Logo",
"squareArt": "Kvadratisk billede",
"selectPoster": "Vælg plakat",
"selectBackground": "Vælg baggrund",
"selectLogo": "Vælg logo",
"selectSquareArt": "Vælg kvadratisk billede",
"fromUrl": "Fra URL",
"uploadFile": "Upload fil",
"enterImageUrl": "Indtast billed-URL",
"imageUrl": "Billed-URL",
"metadataUpdated": "Metadata opdateret",
"metadataUpdateFailed": "Kunne ikke opdatere metadata",
"artworkUpdated": "Grafik opdateret",
"artworkUpdateFailed": "Kunne ikke opdatere grafik",
"noArtworkAvailable": "Ingen grafik tilgængelig",
"notSet": "Ikke indstillet",
"libraryDefault": "Biblioteksstandard",
"accountDefault": "Kontostandard",
"seriesDefault": "Seriestandard",
"episodeSorting": "Episodesortering",
"oldestFirst": "Ældste først",
"newestFirst": "Nyeste først",
"keep": "Behold",
"allEpisodes": "Alle episoder",
"latestEpisodes": "${count} seneste episoder",
"latestEpisode": "Seneste episode",
"episodesAddedPastDays": "Episoder tilføjet de seneste ${count} dage",
"deleteAfterPlaying": "Slet episoder efter afspilning",
"never": "Aldrig",
"afterADay": "Efter en dag",
"afterAWeek": "Efter en uge",
"afterAMonth": "Efter en måned",
"onNextRefresh": "Ved næste opdatering",
"seasons": "Sæsoner",
"show": "Vis",
"hide": "Skjul",
"episodeOrdering": "Episoderækkefølge",
"tmdbAiring": "The Movie Database (Sendt)",
"tvdbAiring": "TheTVDB (Sendt)",
"tvdbAbsolute": "TheTVDB (Absolut)",
"metadataLanguage": "Metadatasprog",
"useOriginalTitle": "Brug originaltitel",
"preferredAudioLanguage": "Foretrukket lydsprog",
"preferredSubtitleLanguage": "Foretrukket undertekstsprog",
"subtitleMode": "Auto-vælg underteksttilstand",
"manuallySelected": "Manuelt valgt",
"shownWithForeignAudio": "Vist med fremmedsproget lyd",
"alwaysEnabled": "Altid aktiveret",
"tags": "Tags",
"addTag": "Tilføj tag",
"genre": "Genre",
"director": "Instruktør",
"writer": "Forfatter",
"producer": "Producer",
"country": "Land",
"collection": "Samling",
"label": "Etiket",
"style": "Stil",
"mood": "Stemning"
},
"serverTasks": {
"title": "Serveropgaver",
"failedToLoad": "Kunne ikke indlæse opgaver",
"noTasks": "Ingen opgaver kører"
},
"trakt": {
"title": "Trakt",
"connected": "Forbundet",
"connectedAs": "Forbundet som @${username}",
"disconnectConfirm": "Frakobl Trakt-konto?",
"disconnectConfirmBody": "Plezy stopper med at sende afspilningsbegivenheder til Trakt. Du kan genoprette forbindelse når som helst.",
"scrobble": "Realtids-scrobbling",
"scrobbleDescription": "Send afspil-, pause- og stop-begivenheder til Trakt under afspilning.",
"watchedSync": "Synkroniser sét-status",
"watchedSyncDescription": "Når du markerer ting som sét i Plezy, markeres de også på Trakt.",
"deviceCodeTitle": "Aktivér Plezy på Trakt",
"deviceCodeBody": "Besøg ${url} og indtast denne kode:",
"openTraktActivate": "Åbn Trakt for at aktivere",
"waitingForAuthorization": "Venter på godkendelse…",
"codeCopied": "Kode kopieret",
"connectFailed": "Kunne ikke forbinde til Trakt. Prøv igen."
}
}
+249 -166
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Mit Plex anmelden",
"showQRCode": "QR-Code anzeigen",
"authenticate": "Authentifizieren",
"debugEnterToken": "Debug: Plex-Token eingeben",
"plexTokenLabel": "Plex-Auth-Token",
"plexTokenHint": "Plex.tv-Token eingeben",
"authenticationTimeout": "Authentifizierung abgelaufen. Bitte erneut versuchen.",
"scanQRToSignIn": "QR-Code scannen zum Anmelden",
"waitingForAuth": "Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.",
@@ -32,8 +29,7 @@
"delete": "Löschen",
"shuffle": "Zufall",
"addTo": "Hinzufügen zu...",
"remove": "Entfernen",
"paste": "Einfügen",
"createNew": "Neu erstellen",
"connect": "Verbinden",
"disconnect": "Trennen",
"play": "Abspielen",
@@ -41,12 +37,11 @@
"resume": "Fortsetzen",
"error": "Fehler",
"search": "Suche",
"home": "Startseite",
"home": "Start",
"back": "Zurück",
"settings": "Einstellungen",
"settings": "Optionen",
"mute": "Stumm",
"ok": "OK",
"loading": "Laden...",
"reconnect": "Erneut verbinden",
"exitConfirmTitle": "App beenden?",
"exitConfirmMessage": "Möchtest du die App wirklich beenden?",
@@ -57,13 +52,14 @@
"refreshingServers": "Server werden aktualisiert...",
"loadingServers": "Server werden geladen...",
"connectingToServers": "Verbindung zu Servern...",
"startingOfflineMode": "Offlinemodus wird gestartet..."
"startingOfflineMode": "Offlinemodus wird gestartet...",
"loading": "Laden..."
},
"screens": {
"licenses": "Lizenzen",
"switchProfile": "Profil wechseln",
"subtitleStyling": "Untertitel-Stil",
"mpvConfig": "MPV-Konfiguration",
"mpvConfig": "mpv.conf",
"logs": "Protokolle"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Einstellungen",
"supportDeveloper": "Plezy unterstützen",
"supportDeveloperDescription": "Per Liberapay spenden, um die Entwicklung zu fördern",
"language": "Sprache",
"theme": "Design",
"appearance": "Darstellung",
"videoPlayback": "Videowiedergabe",
"videoPlaybackDescription": "Wiedergabeverhalten konfigurieren",
"advanced": "Erweitert",
"episodePosterMode": "Episoden-Poster-Stil",
"seriesPoster": "Serienposter",
"seriesPosterDescription": "Zeige das Serienposter für alle Episoden",
"seasonPoster": "Staffelposter",
"seasonPosterDescription": "Zeige das staffelspezifische Poster für Episoden",
"episodeThumbnail": "Episoden-Miniatur",
"episodeThumbnailDescription": "Zeige 16:9 Episoden-Vorschaubilder",
"episodeThumbnail": "Miniatur",
"showHeroSectionDescription": "Bereich mit empfohlenen Inhalten auf der Startseite anzeigen",
"secondsLabel": "Sekunden",
"minutesLabel": "Minuten",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Dauer eingeben (${min}-${max})",
"systemTheme": "System",
"systemThemeDescription": "Systemeinstellungen folgen",
"lightTheme": "Hell",
"darkTheme": "Dunkel",
"oledTheme": "OLED",
"oledThemeDescription": "Reines Schwarz für OLED-Bildschirme",
"libraryDensity": "Mediathekdichte",
"compact": "Kompakt",
"compactDescription": "Kleinere Karten, mehr Elemente sichtbar",
"normal": "Normal",
"normalDescription": "Standardgröße",
"comfortable": "Großzügig",
"comfortableDescription": "Größere Karten, weniger Elemente sichtbar",
"viewMode": "Ansichtsmodus",
"gridView": "Raster",
"gridViewDescription": "Elemente im Raster anzeigen",
"listView": "Liste",
"listViewDescription": "Elemente in Listenansicht anzeigen",
"showHeroSection": "Hero-Bereich anzeigen",
"useGlobalHubs": "Plex-Startseiten-Layout verwenden",
"useGlobalHubsDescription": "Zeigt Startseiten-Hubs wie der offizielle Plex-Client. Wenn deaktiviert, werden stattdessen Empfehlungen pro Bibliothek angezeigt.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Vorschaubilder unscharf machen und Beschreibungen für noch nicht gesehene Episoden ausblenden",
"playerBackend": "Player-Backend",
"exoPlayer": "ExoPlayer (Empfohlen)",
"exoPlayerDescription": "Android-nativer Player mit besserer Hardware-Unterstützung",
"mpv": "MPV",
"mpvDescription": "Erweiterter Player mit mehr Funktionen und ASS-Untertitel-Unterstützung",
"mpv": "mpv",
"hardwareDecoding": "Hardware-Decodierung",
"hardwareDecodingDescription": "Hardwarebeschleunigung verwenden, sofern verfügbar",
"bufferSize": "Puffergröße",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Aussehen von Untertiteln anpassen",
"smallSkipDuration": "Kleine Sprungdauer",
"largeSkipDuration": "Große Sprungdauer",
"rewindOnResume": "Zurückspulen bei Fortsetzung",
"secondsUnit": "${seconds} Sekunden",
"defaultSleepTimer": "Standard-Sleep-Timer",
"minutesUnit": "${minutes} Minuten",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Tastenkürzel anpassen",
"videoPlayerNavigation": "Videoplayer-Navigation",
"videoPlayerNavigationDescription": "Pfeiltasten zur Navigation der Videoplayer-Steuerung verwenden",
"watchTogetherRelay": "Gemeinsam Schauen Relay",
"watchTogetherRelayDescription": "Einen benutzerdefinierten Relay-Server für Gemeinsam Schauen festlegen. Alle Teilnehmer müssen denselben Server verwenden.",
"watchTogetherRelayHint": "https://mein-relay.beispiel.de",
"crashReporting": "Absturzberichte",
"crashReportingDescription": "Absturzberichte senden, um die App zu verbessern",
"debugLogging": "Debug-Protokollierung",
"debugLoggingDescription": "Detaillierte Protokolle zur Fehleranalyse aktivieren",
"viewLogs": "Protokolle anzeigen",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Abspann automatisch überspringen und nächste Episode abspielen",
"autoSkipDelay": "Verzögerung für automatisches Überspringen",
"autoSkipDelayDescription": "${seconds} Sekunden vor dem automatischen Überspringen warten",
"introPattern": "Intro-Markierungsmuster",
"introPatternDescription": "Regulärer Ausdruck zum Erkennen von Intro-Markierungen in Kapiteltiteln",
"creditsPattern": "Abspann-Markierungsmuster",
"creditsPatternDescription": "Regulärer Ausdruck zum Erkennen von Abspann-Markierungen in Kapiteltiteln",
"invalidRegex": "Ungültiger regulärer Ausdruck",
"downloads": "Downloads",
"downloadLocationDescription": "Speicherort für heruntergeladene Inhalte wählen",
"downloadLocationDefault": "Standard (App-Speicher)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Ordnerauswahl fehlgeschlagen",
"downloadOnWifiOnly": "Nur über WLAN herunterladen",
"downloadOnWifiOnlyDescription": "Downloads über mobile Daten verhindern",
"autoRemoveWatchedDownloads": "Gesehene Downloads automatisch entfernen",
"autoRemoveWatchedDownloadsDescription": "Heruntergeladene Episoden und Filme automatisch löschen, wenn sie als gesehen markiert werden",
"cellularDownloadBlocked": "Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.",
"maxVolume": "Maximale Lautstärke",
"maxVolumeDescription": "Lautstärke über 100% für leise Medien erlauben",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Zeige auf Discord, was du gerade schaust",
"trakt": "Trakt",
"traktDescription": "Wiedergabeverlauf mit Trakt synchronisieren",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern",
"autoPip": "Automatisches Bild-in-Bild",
"autoPipDescription": "Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird",
"matchContentFrameRate": "Inhalts-Bildrate anpassen",
"matchContentFrameRateDescription": "Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Bildwiederholrate anpassen",
"matchRefreshRateDescription": "Bildwiederholrate des Displays an den Videoinhalt im Vollbildmodus anpassen",
"matchDynamicRange": "Dynamikumfang anpassen",
"matchDynamicRangeDescription": "HDR für HDR-Inhalte automatisch aktivieren und beim Verlassen des Players auf SDR zurücksetzen",
"displaySwitchDelay": "Verzögerung beim Displaywechsel",
"tunneledPlayback": "Tunnelwiedergabe",
"tunneledPlaybackDescription": "Hardware-beschleunigte Video-Tunnelierung verwenden. Deaktivieren, wenn bei HDR-Inhalten ein schwarzer Bildschirm mit Ton erscheint",
"requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen",
"requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen",
"confirmExitOnBack": "Vor dem Beenden bestätigen",
"confirmExitOnBackDescription": "Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird"
"confirmExitOnBackDescription": "Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird",
"forceTvMode": "TV-Modus erzwingen",
"forceTvModeDescription": "TV-Layout unabhängig von der automatischen Erkennung verwenden. Nützlich für Android-TV-Geräte, die das Leanback-Feature nicht melden. Startet die App bei Änderung neu.",
"autoHidePerformanceOverlay": "Leistungsoverlay automatisch ausblenden",
"autoHidePerformanceOverlayDescription": "Leistungsoverlay mit den Wiedergabesteuerungen ein-/ausblenden",
"showNavBarLabels": "Navigationsleisten-Beschriftungen anzeigen",
"showNavBarLabelsDescription": "Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen",
"liveTvDefaultFavorites": "Standardmäßig Favoritenkanäle",
"liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Filme, Serien, Musik suchen...",
@@ -239,11 +263,6 @@
"skipMarker": "Intro/Abspann überspringen"
}
},
"pinEntry": {
"enterPin": "PIN eingeben",
"showPin": "PIN anzeigen",
"hidePin": "PIN verbergen"
},
"fileInfo": {
"title": "Dateiinfo",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Primärfarben",
"chromaSubsampling": "Chroma-Subsampling",
"channels": "Kanäle",
"subtitles": "Untertitel",
"overallBitrate": "Gesamt-Bitrate",
"path": "Pfad",
"size": "Größe",
"container": "Container",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Dies umfasst alle Episoden und deren Dateien.",
"mediaDeletedSuccessfully": "Medienelement erfolgreich gelöscht",
"mediaFailedToDelete": "Löschen des Medienelements fehlgeschlagen",
"rate": "Bewerten"
"rate": "Bewerten",
"playFromBeginning": "Von Anfang an abspielen",
"playVersion": "Version abspielen..."
},
"accessibility": {
"mediaCardMovie": "${title}, Film",
@@ -316,7 +339,6 @@
"unlockRotation": "Rotation entsperren",
"timerActive": "Schlaftimer aktiv",
"playbackWillPauseIn": "Wiedergabe wird in ${duration} pausiert",
"sleepTimerCompleted": "Schlaftimer abgelaufen Wiedergabe pausiert",
"stillWatching": "Schaust du noch?",
"pausingIn": "Pause in ${seconds}s",
"continueWatching": "Weiter",
@@ -333,34 +355,43 @@
"muteButton": "Stumm schalten",
"unmuteButton": "Stummschaltung aufheben",
"settingsButton": "Videoeinstellungen",
"audioTrackButton": "Tonspuren",
"subtitlesButton": "Untertitel",
"tracksButton": "Audio und Untertitel",
"chaptersButton": "Kapitel",
"versionsButton": "Videoversionen",
"pipButton": "Bild-in-Bild Modus",
"aspectRatioButton": "Seitenverhältnis",
"ambientLighting": "Umgebungsbeleuchtung",
"ambientLightingOn": "Umgebungsbeleuchtung aktivieren",
"ambientLightingOff": "Umgebungsbeleuchtung deaktivieren",
"fullscreenButton": "Vollbild aktivieren",
"exitFullscreenButton": "Vollbild verlassen",
"alwaysOnTopButton": "Immer im Vordergrund",
"rotationLockButton": "Dreh­sperre",
"lockScreen": "Bildschirm sperren",
"screenLockButton": "Bildschirmsperre",
"longPressToUnlock": "Lange drücken zum Entsperren",
"timelineSlider": "Video-Zeitleiste",
"volumeSlider": "Lautstärkepegel",
"endsAt": "Endet um ${time}",
"pipActive": "Wiedergabe im Bild-in-Bild-Modus",
"pipFailed": "Bild-in-Bild konnte nicht gestartet werden",
"pipErrors": {
"androidVersion": "Erfordert Android 8.0 oder neuer",
"iosVersion": "Erfordert iOS 15.0 oder neuer",
"permissionDisabled": "Bild-in-Bild-Berechtigung ist deaktiviert. Aktiviere sie unter Einstellungen > Apps > Plezy > Bild-in-Bild",
"notSupported": "Dieses Gerät unterstützt den Bild-in-Bild-Modus nicht",
"voSwitchFailed": "Videoausgabe für Bild-in-Bild konnte nicht umgeschaltet werden",
"failed": "Bild-in-Bild konnte nicht gestartet werden",
"unknown": "Ein Fehler ist aufgetreten: ${error}"
},
"chapters": "Kapitel",
"noChaptersAvailable": "Keine Kapitel verfügbar",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "Warteschlange",
"noQueueItems": "Keine Elemente in der Warteschlange",
"searchSubtitles": "Untertitel suchen",
"language": "Sprache",
"noSubtitlesFound": "Keine Untertitel gefunden",
"subtitleDownloaded": "Untertitel heruntergeladen",
"subtitleDownloadFailed": "Untertitel konnte nicht heruntergeladen werden",
"searchLanguages": "Sprachen suchen..."
},
"userStatus": {
"admin": "Eigentümer",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Als ungesehen markiert",
"markedAsWatchedOffline": "Als gesehen markiert (wird synchronisiert, wenn online)",
"markedAsUnwatchedOffline": "Als ungesehen markiert (wird synchronisiert, wenn online)",
"autoRemovedWatchedDownload": "Automatisch entfernt: ${title}",
"removedFromContinueWatching": "Aus Weiterschauen' entfernt",
"errorLoading": "Fehler: ${error}",
"fileInfoNotAvailable": "Dateiinfo nicht verfügbar",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Wiedergabewarteschlange konnte nicht erstellt werden keine Elemente",
"failedPlayback": "Wiedergabe für ${action} fehlgeschlagen: ${error}",
"switchingToCompatiblePlayer": "Wechsle zu kompatiblem Player...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Wiedergabe fehlgeschlagen",
"serverLimitBody": "Der Server hat einen Fehler zurückgegeben (HTTP 500). Das bedeutet meist, dass der Serverbesitzer ein Bandbreiten- oder Transcoding-Limit festgelegt hat, das deine Sitzung ablehnt. Vom Client aus lässt sich nichts machen — der Serverbesitzer muss seine Einstellungen anpassen.",
"logsUploaded": "Protokolle hochgeladen",
"logsUploadFailed": "Protokolle konnten nicht hochgeladen werden",
"logId": "Protokoll-ID"
},
"subtitlingStyling": {
"stylingOptions": "Stiloptionen",
"text": "Text",
"border": "Rahmen",
"background": "Hintergrund",
"fontSize": "Schriftgröße",
"textColor": "Textfarbe",
"borderSize": "Rahmengröße",
"borderColor": "Rahmenfarbe",
"backgroundOpacity": "Hintergrunddeckkraft",
"backgroundColor": "Hintergrundfarbe",
"position": "Position"
"position": "Position",
"assOverride": "ASS-Überschreibung",
"bold": "Fett",
"italic": "Kursiv"
},
"mpvConfig": {
"title": "MPV-Konfiguration",
"title": "mpv-Konfiguration",
"description": "Erweiterte Videoplayer-Einstellungen",
"properties": "Eigenschaften",
"presets": "Voreinstellungen",
"noProperties": "Keine Eigenschaften konfiguriert",
"noPresets": "Keine gespeicherten Voreinstellungen",
"addProperty": "Eigenschaft hinzufügen",
"editProperty": "Eigenschaft bearbeiten",
"deleteProperty": "Eigenschaft löschen",
"propertyKey": "Eigenschaftsschlüssel",
"propertyKeyHint": "z.B. hwdec, demuxer-max-bytes",
"propertyValue": "Eigenschaftswert",
"propertyValueHint": "z.B. auto, 256000000",
"saveAsPreset": "Als Voreinstellung speichern...",
"presetName": "Name der Voreinstellung",
"presetNameHint": "Namen für diese Voreinstellung eingeben",
@@ -436,8 +466,7 @@
"presetLoaded": "Voreinstellung geladen",
"presetDeleted": "Voreinstellung gelöscht",
"confirmDeletePreset": "Möchten Sie diese Voreinstellung wirklich löschen?",
"confirmDeleteProperty": "Möchten Sie diese Eigenschaft wirklich löschen?",
"entriesCount": "${count} Einträge"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Aktion bestätigen"
@@ -452,11 +481,8 @@
"overview": "Übersicht",
"cast": "Besetzung",
"extras": "Trailer & Extras",
"seasons": "Staffeln",
"studio": "Studio",
"rating": "Altersfreigabe",
"episodeCount": "${count} Episoden",
"watchedProgress": "${watched} von ${total} gesehen",
"movie": "Film",
"tvShow": "Serie",
"minutesLeft": "${minutes} Min übrig"
@@ -518,6 +544,7 @@
"playlists": "Wiedergabelisten"
},
"groupings": {
"title": "Gruppierung",
"all": "Alle",
"movies": "Filme",
"shows": "Serien",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Protokolle löschen",
"copyLogs": "Protokolle kopieren",
"uploadLogs": "Upload Logs",
"error": "Fehler:",
"stackTrace": "Stacktrace:"
"uploadLogs": "Protokolle hochladen"
},
"licenses": {
"relatedPackages": "Verwandte Pakete",
@@ -559,26 +584,18 @@
"licensesCount": "${count} Lizenzen"
},
"navigation": {
"libraries": "Mediatheken",
"libraries": "Medien",
"downloads": "Downloads",
"liveTv": "Live-TV"
},
"liveTv": {
"title": "Live-TV",
"channels": "Kanäle",
"guide": "Programmführer",
"noChannels": "Keine Kanäle verfügbar",
"noDvr": "Kein DVR auf einem Server konfiguriert",
"tuneFailed": "Kanal konnte nicht eingestellt werden",
"loading": "Kanäle werden geladen...",
"nowPlaying": "Läuft gerade",
"noPrograms": "Keine Programmdaten verfügbar",
"channelNumber": "Kanal ${number}",
"live": "LIVE",
"hd": "HD",
"premiere": "NEU",
"reloadGuide": "Programmführer neu laden",
"allChannels": "Alle Kanäle",
"now": "Jetzt",
"today": "Heute",
"midnight": "Mitternacht",
@@ -588,30 +605,41 @@
"evening": "Abend",
"lateNight": "Spätnacht",
"whatsOn": "Jetzt im TV",
"watchChannel": "Kanal ansehen"
"watchChannel": "Kanal ansehen",
"favorites": "Favoriten",
"reorderFavorites": "Favoriten sortieren",
"joinSession": "Laufender Sitzung beitreten",
"watchFromStart": "Von Anfang an ansehen (vor ${minutes} Min.)",
"watchLive": "Live ansehen",
"goToLive": "Zum Live-Bild"
},
"downloads": {
"title": "Downloads",
"manage": "Verwalten",
"tvShows": "Serien",
"movies": "Filme",
"noDownloads": "Noch keine Downloads",
"noDownloadsDescription": "Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt",
"downloadNow": "Herunterladen",
"deleteDownload": "Download löschen",
"retryDownload": "Download wiederholen",
"downloadQueued": "Download in Warteschlange",
"episodesQueued": "${count} Episoden zum Download hinzugefügt",
"downloadDeleted": "Download gelöscht",
"deleteConfirm": "Möchtest du \"${title}\" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.",
"deletingWithProgress": "Lösche ${title}... (${current} von ${total})",
"noDownloadsTree": "Keine Downloads",
"pauseAll": "Alle pausieren",
"resumeAll": "Alle fortsetzen",
"deleteAll": "Alle löschen"
"collections": {
"title": "Sammlungen",
"collection": "Sammlung",
"empty": "Sammlung ist leer",
"unknownLibrarySection": "Löschen nicht möglich: Unbekannte Bibliothekssektion",
"deleteCollection": "Sammlung löschen",
"deleteConfirm": "Sind Sie sicher, dass Sie \"${title}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
"deleted": "Sammlung gelöscht",
"deleteFailed": "Sammlung konnte nicht gelöscht werden",
"deleteFailedWithError": "Sammlung konnte nicht gelöscht werden: ${error}",
"failedToLoadItems": "Sammlungselemente konnten nicht geladen werden: ${error}",
"selectCollection": "Sammlung auswählen",
"collectionName": "Sammlungsname",
"enterCollectionName": "Sammlungsnamen eingeben",
"addedToCollection": "Zur Sammlung hinzugefügt",
"errorAddingToCollection": "Fehler beim Hinzufügen zur Sammlung",
"created": "Sammlung erstellt",
"removeFromCollection": "Aus Sammlung entfernen",
"removeFromCollectionConfirm": "\"${title}\" aus dieser Sammlung entfernen?",
"removedFromCollection": "Aus Sammlung entfernt",
"removeFromCollectionFailed": "Entfernen aus Sammlung fehlgeschlagen",
"removeFromCollectionError": "Fehler beim Entfernen aus der Sammlung: ${error}",
"searchCollections": "Sammlungen durchsuchen..."
},
"playlists": {
"title": "Wiedergabelisten",
"playlist": "Wiedergabeliste",
"noPlaylists": "Keine Wiedergabelisten gefunden",
"create": "Wiedergabeliste erstellen",
"playlistName": "Name der Wiedergabeliste",
@@ -629,38 +657,12 @@
"itemAdded": "Zur Wiedergabeliste hinzugefügt",
"itemRemoved": "Aus Wiedergabeliste entfernt",
"selectPlaylist": "Wiedergabeliste auswählen",
"createNewPlaylist": "Neue Wiedergabeliste erstellen",
"errorCreating": "Wiedergabeliste konnte nicht erstellt werden",
"errorDeleting": "Wiedergabeliste konnte nicht gelöscht werden",
"errorLoading": "Wiedergabelisten konnten nicht geladen werden",
"errorAdding": "Konnte nicht zur Wiedergabeliste hinzugefügt werden",
"errorReordering": "Element der Wiedergabeliste konnte nicht neu geordnet werden",
"errorRemoving": "Konnte nicht aus der Wiedergabeliste entfernt werden",
"playlist": "Wiedergabeliste"
},
"collections": {
"title": "Sammlungen",
"collection": "Sammlung",
"empty": "Sammlung ist leer",
"unknownLibrarySection": "Löschen nicht möglich: Unbekannte Bibliothekssektion",
"deleteCollection": "Sammlung löschen",
"deleteConfirm": "Sind Sie sicher, dass Sie \"${title}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
"deleted": "Sammlung gelöscht",
"deleteFailed": "Sammlung konnte nicht gelöscht werden",
"deleteFailedWithError": "Sammlung konnte nicht gelöscht werden: ${error}",
"failedToLoadItems": "Sammlungselemente konnten nicht geladen werden: ${error}",
"selectCollection": "Sammlung auswählen",
"createNewCollection": "Neue Sammlung erstellen",
"collectionName": "Sammlungsname",
"enterCollectionName": "Sammlungsnamen eingeben",
"addedToCollection": "Zur Sammlung hinzugefügt",
"errorAddingToCollection": "Fehler beim Hinzufügen zur Sammlung",
"created": "Sammlung erstellt",
"removeFromCollection": "Aus Sammlung entfernen",
"removeFromCollectionConfirm": "\"${title}\" aus dieser Sammlung entfernen?",
"removedFromCollection": "Aus Sammlung entfernt",
"removeFromCollectionFailed": "Entfernen aus Sammlung fehlgeschlagen",
"removeFromCollectionError": "Fehler beim Entfernen aus der Sammlung: ${error}"
"errorRemoving": "Konnte nicht aus der Wiedergabeliste entfernt werden"
},
"watchTogether": {
"title": "Gemeinsam Schauen",
@@ -697,18 +699,76 @@
"leave": "Verlassen",
"syncing": "Synchronisieren...",
"joinWatchSession": "Schausitzung Beitreten",
"enterCodeHint": "8-stelligen Code eingeben",
"enterCodeHint": "5-stelligen Code eingeben",
"pasteFromClipboard": "Aus Zwischenablage einfügen",
"pleaseEnterCode": "Bitte gib einen Sitzungscode ein",
"codeMustBe8Chars": "Sitzungscode muss 8 Zeichen haben",
"codeMustBe5Chars": "Sitzungscode muss 5 Zeichen haben",
"joinInstructions": "Gib den vom Host geteilten Sitzungscode ein, um seiner Schausitzung beizutreten.",
"failedToCreate": "Sitzung konnte nicht erstellt werden",
"failedToJoin": "Sitzung konnte nicht beigetreten werden",
"sessionCodeCopied": "Sitzungscode in Zwischenablage kopiert",
"relayUnreachable": "Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.",
"reconnectingToHost": "Verbindung zum Host wird wiederhergestellt...",
"currentPlayback": "Aktuelle Wiedergabe",
"joinCurrentPlayback": "Aktuelle Wiedergabe beitreten",
"joinCurrentPlaybackDescription": "Springe zurück zu dem, was der Host gerade ansieht",
"failedToOpenCurrentPlayback": "Aktuelle Wiedergabe konnte nicht geöffnet werden",
"participantJoined": "${name} ist beigetreten",
"participantLeft": "${name} hat die Sitzung verlassen"
"participantLeft": "${name} hat die Sitzung verlassen",
"participantPaused": "${name} hat pausiert",
"participantResumed": "${name} hat fortgesetzt",
"participantSeeked": "${name} hat gespult",
"participantBuffering": "${name} puffert",
"waitingForParticipants": "Warte auf andere zum Laden...",
"recentRooms": "Letzte Räume",
"renameRoom": "Raum umbenennen",
"removeRoom": "Entfernen"
},
"downloads": {
"title": "Downloads",
"manage": "Verwalten",
"tvShows": "Serien",
"movies": "Filme",
"noDownloads": "Noch keine Downloads",
"noDownloadsDescription": "Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt",
"downloadNow": "Herunterladen",
"deleteDownload": "Download löschen",
"retryDownload": "Download wiederholen",
"downloadQueued": "Download in Warteschlange",
"serverErrorBitrate": "Serverfehler — die Datei überschreitet möglicherweise das Remote-Streaming-Bitratenlimit",
"episodesQueued": "${count} Episoden zum Download hinzugefügt",
"downloadDeleted": "Download gelöscht",
"deleteConfirm": "Möchtest du \"${title}\" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.",
"deletingWithProgress": "Lösche ${title}... (${current} von ${total})",
"noDownloadsTree": "Keine Downloads",
"pauseAll": "Alle pausieren",
"resumeAll": "Alle fortsetzen",
"deleteAll": "Alle löschen",
"selectVersion": "Version auswählen",
"allEpisodes": "Alle Episoden",
"unwatchedOnly": "Nur ungesehene",
"nextNUnwatched": "Nächste ${count} ungesehene",
"customAmount": "Eigene Anzahl...",
"howManyEpisodes": "Wie viele Episoden?",
"itemsQueued": "${count} Elemente zum Download eingereiht",
"keepSynced": "Synchronisiert halten",
"downloadOnce": "Einmal herunterladen",
"keepNUnwatched": "${count} ungesehene behalten",
"editSyncRule": "Sync-Regel bearbeiten",
"removeSyncRule": "Sync-Regel entfernen",
"removeSyncRuleConfirm": "Synchronisierung von \"${title}\" beenden? Heruntergeladene Episoden werden behalten.",
"syncRuleCreated": "Sync-Regel erstellt — ${count} ungesehene Episoden werden behalten",
"syncRuleUpdated": "Sync-Regel aktualisiert",
"syncRuleRemoved": "Sync-Regel entfernt",
"syncedNewEpisodes": "${count} neue Episoden für ${title} synchronisiert",
"activeSyncRules": "Sync-Regeln",
"noSyncRules": "Keine Sync-Regeln",
"manageSyncRule": "Synchronisierung verwalten",
"editEpisodeCount": "Episodenanzahl",
"editSyncFilter": "Synchronisierungsfilter",
"syncAllItems": "Alle Einträge synchronisieren",
"syncUnwatchedItems": "Ungesehene Einträge synchronisieren",
"syncRuleListCreated": "Sync-Regel erstellt"
},
"shaders": {
"title": "Shader",
@@ -716,58 +776,46 @@
"nvscalerDescription": "NVIDIA-Bildskalierung für schärferes Video",
"qualityFast": "Schnell",
"qualityHQ": "Hohe Qualität",
"mode": "Modus"
"mode": "Modus",
"importShader": "Shader importieren",
"customShaderDescription": "Benutzerdefinierter GLSL-Shader",
"shaderImported": "Shader importiert",
"shaderImportFailed": "Shader konnte nicht importiert werden",
"deleteShader": "Shader löschen",
"deleteShaderConfirm": "\"${name}\" löschen?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Mit Gerät verbinden",
"hostRemoteSession": "Fernsteuerungssitzung starten",
"controlThisDevice": "Dieses Gerät mit dem Handy steuern",
"remoteControl": "Fernsteuerung",
"controlDesktop": "Ein Desktop-Gerät steuern",
"title": "Companion-Fernbedienung",
"connectedTo": "Verbunden mit ${name}",
"session": {
"creatingSession": "Fernsteuerungssitzung wird erstellt...",
"failedToCreate": "Fernsteuerungssitzung konnte nicht erstellt werden:",
"noSession": "Keine Sitzung verfügbar",
"scanQrCode": "QR-Code scannen",
"orEnterManually": "Oder manuell eingeben",
"startingServer": "Remote-Server wird gestartet...",
"failedToCreate": "Remote-Server konnte nicht gestartet werden:",
"hostAddress": "Host-Adresse",
"sessionId": "Sitzungs-ID",
"pin": "PIN",
"connected": "Verbunden",
"waitingForConnection": "Warte auf Verbindung...",
"serverRunning": "Remote-Server aktiv",
"serverStopped": "Remote-Server gestoppt",
"serverRunningDescription": "Mobilgeräte in deinem Netzwerk können diese App finden und sich verbinden",
"serverStoppedDescription": "Starte den Server, um Mobilgeräten die Verbindung zu ermöglichen",
"usePhoneToControl": "Verwende dein Mobilgerät, um diese App zu steuern",
"copiedToClipboard": "${label} in Zwischenablage kopiert",
"copyToClipboard": "In Zwischenablage kopieren",
"newSession": "Neue Sitzung",
"startServer": "Server starten",
"stopServer": "Server stoppen",
"minimize": "Minimieren"
},
"pairing": {
"scan": "Scannen",
"manual": "Manuell",
"pairWithDesktop": "Mit Desktop koppeln",
"enterSessionDetails": "Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden",
"discoveryDescription": "Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8-stellige Sitzungs-ID eingeben",
"pinHint": "6-stellige PIN eingeben",
"connecting": "Verbinde...",
"tips": "Tipps",
"tipDesktop": "Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü",
"tipScan": "Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln",
"tipWifi": "Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind",
"cameraPermissionRequired": "Kameraberechtigung wird zum Scannen von QR-Codes benötigt.\nBitte erteile den Kamerazugriff in den Geräteeinstellungen.",
"cameraError": "Kamera konnte nicht gestartet werden: ${error}",
"scanInstruction": "Richte deine Kamera auf den QR-Code auf deinem Desktop",
"invalidQrCode": "Ungültiges QR-Code-Format",
"connecting": "Verbinden...",
"searchingForDevices": "Suche nach Geräten...",
"noDevicesFound": "Keine Geräte in deinem Netzwerk gefunden",
"noDevicesHint": "Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind",
"availableDevices": "Verfügbare Geräte",
"manualConnection": "Manuelle Verbindung",
"cryptoInitFailed": "Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.",
"validationHostRequired": "Bitte Host-Adresse eingeben",
"validationHostFormat": "Format muss IP:Port sein (z.B. 192.168.1.100:48632)",
"validationSessionIdRequired": "Bitte Sitzungs-ID eingeben",
"validationSessionIdLength": "Sitzungs-ID muss 8 Zeichen lang sein",
"validationPinRequired": "Bitte PIN eingeben",
"validationPinLength": "PIN muss 6 Ziffern haben",
"connectionTimedOut": "Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.",
"sessionNotFound": "Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.",
"connectionTimedOut": "Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.",
"sessionNotFound": "Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.",
"authFailed": "Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.",
"failedToConnect": "Verbindung fehlgeschlagen: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Verbindung wird wiederhergestellt...",
"attemptOf": "Versuch ${current} von 5",
"retryNow": "Jetzt wiederholen",
"connectionError": "Verbindungsfehler",
"notConnected": "Nicht verbunden",
"tabRemote": "Fernbedienung",
"tabPlay": "Wiedergabe",
"tabMore": "Mehr",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Externen Player verwenden",
"useExternalPlayerDescription": "Videos in einer externen App statt im integrierten Player öffnen",
"selectPlayer": "Player auswählen",
"customPlayers": "Benutzerdefinierte Player",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Benutzerdefinierten Player hinzufügen",
"playerName": "Playername",
"playerCommand": "Befehl",
"playerPackage": "Paketname",
"playerUrlScheme": "URL-Schema",
"customPlayer": "Benutzerdefinierter Player",
"off": "Aus",
"launchFailed": "Externer Player konnte nicht geöffnet werden",
"appNotInstalled": "${name} ist nicht installiert",
@@ -847,8 +893,12 @@
"summary": "Zusammenfassung",
"poster": "Poster",
"background": "Hintergrund",
"logo": "Logo",
"squareArt": "Quadratisches Bild",
"selectPoster": "Poster auswählen",
"selectBackground": "Hintergrund auswählen",
"selectLogo": "Logo auswählen",
"selectSquareArt": "Quadratisches Bild auswählen",
"fromUrl": "Von URL",
"uploadFile": "Datei hochladen",
"enterImageUrl": "Bild-URL eingeben",
@@ -890,6 +940,39 @@
"subtitleMode": "Automatische Untertitelauswahl",
"manuallySelected": "Manuell ausgewählt",
"shownWithForeignAudio": "Bei fremdsprachigem Audio anzeigen",
"alwaysEnabled": "Immer aktiviert"
"alwaysEnabled": "Immer aktiviert",
"tags": "Tags",
"addTag": "Tag hinzufügen",
"genre": "Genre",
"director": "Regisseur",
"writer": "Autor",
"producer": "Produzent",
"country": "Land",
"collection": "Sammlung",
"label": "Label",
"style": "Stil",
"mood": "Stimmung"
},
"serverTasks": {
"title": "Serveraufgaben",
"failedToLoad": "Aufgaben konnten nicht geladen werden",
"noTasks": "Keine laufenden Aufgaben"
},
"trakt": {
"title": "Trakt",
"connected": "Verbunden",
"connectedAs": "Verbunden als @${username}",
"disconnectConfirm": "Trakt-Konto trennen?",
"disconnectConfirmBody": "Plezy sendet keine Wiedergabe-Ereignisse mehr an Trakt. Du kannst dich jederzeit erneut verbinden.",
"scrobble": "Echtzeit-Scrobbling",
"scrobbleDescription": "Sende Play-, Pause- und Stopp-Ereignisse während der Wiedergabe an Trakt.",
"watchedSync": "Gesehen-Status synchronisieren",
"watchedSyncDescription": "Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert.",
"deviceCodeTitle": "Plezy auf Trakt aktivieren",
"deviceCodeBody": "Öffne ${url} und gib diesen Code ein:",
"openTraktActivate": "Trakt zum Aktivieren öffnen",
"waitingForAuthorization": "Warte auf Autorisierung…",
"codeCopied": "Code kopiert",
"connectFailed": "Verbindung zu Trakt fehlgeschlagen. Versuche es erneut."
}
}
+196 -113
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Sign in with Plex",
"showQRCode": "Show QR Code",
"authenticate": "Authenticate",
"debugEnterToken": "Debug: Enter Plex Token",
"plexTokenLabel": "Plex Auth Token",
"plexTokenHint": "Enter your Plex.tv token",
"authenticationTimeout": "Authentication timed out. Please try again.",
"scanQRToSignIn": "Scan this QR code to sign in",
"waitingForAuth": "Waiting for authentication...\nPlease complete sign-in in your browser.",
@@ -32,8 +29,7 @@
"delete": "Delete",
"shuffle": "Shuffle",
"addTo": "Add to...",
"remove": "Remove",
"paste": "Paste",
"createNew": "Create new",
"connect": "Connect",
"disconnect": "Disconnect",
"play": "Play",
@@ -46,7 +42,6 @@
"settings": "Settings",
"mute": "Mute",
"ok": "OK",
"loading": "Loading...",
"reconnect": "Reconnect",
"exitConfirmTitle": "Exit app?",
"exitConfirmMessage": "Are you sure you want to exit?",
@@ -57,13 +52,14 @@
"refreshingServers": "Refreshing servers...",
"loadingServers": "Loading servers...",
"connectingToServers": "Connecting to servers...",
"startingOfflineMode": "Starting offline mode..."
"startingOfflineMode": "Starting offline mode...",
"loading": "Loading..."
},
"screens": {
"licenses": "Licenses",
"switchProfile": "Switch Profile",
"subtitleStyling": "Subtitle Styling",
"mpvConfig": "MPV Configuration",
"mpvConfig": "mpv.conf",
"logs": "Logs"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Settings",
"supportDeveloper": "Support Plezy",
"supportDeveloperDescription": "Donate via Liberapay to fund development",
"language": "Language",
"theme": "Theme",
"appearance": "Appearance",
"videoPlayback": "Video Playback",
"videoPlaybackDescription": "Configure playback behavior",
"advanced": "Advanced",
"episodePosterMode": "Episode Poster Style",
"seriesPoster": "Series Poster",
"seriesPosterDescription": "Show the series poster for all episodes",
"seasonPoster": "Season Poster",
"seasonPosterDescription": "Show the season-specific poster for episodes",
"episodeThumbnail": "Episode Thumbnail",
"episodeThumbnailDescription": "Show 16:9 episode screenshot thumbnails",
"episodeThumbnail": "Thumbnail",
"showHeroSectionDescription": "Display featured content carousel on home screen",
"secondsLabel": "Seconds",
"minutesLabel": "Minutes",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Enter duration (${min}-${max})",
"systemTheme": "System",
"systemThemeDescription": "Follow system settings",
"lightTheme": "Light",
"darkTheme": "Dark",
"oledTheme": "OLED",
"oledThemeDescription": "Pure black for OLED screens",
"libraryDensity": "Library Density",
"compact": "Compact",
"compactDescription": "Smaller cards, more items visible",
"normal": "Normal",
"normalDescription": "Default size",
"comfortable": "Comfortable",
"comfortableDescription": "Larger cards, fewer items visible",
"viewMode": "View Mode",
"gridView": "Grid",
"gridViewDescription": "Display items in a grid layout",
"listView": "List",
"listViewDescription": "Display items in a list layout",
"showHeroSection": "Show Hero Section",
"useGlobalHubs": "Use Plex Home Layout",
"useGlobalHubsDescription": "Show home page hubs like the official Plex client. When off, shows per-library recommendations instead.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Blur thumbnails and hide descriptions for episodes you haven't watched yet",
"playerBackend": "Player Backend",
"exoPlayer": "ExoPlayer (Recommended)",
"exoPlayerDescription": "Android native player with better hardware support",
"mpv": "MPV",
"mpvDescription": "Advanced player with more features and ASS subtitle support",
"mpv": "mpv",
"hardwareDecoding": "Hardware Decoding",
"hardwareDecodingDescription": "Use hardware acceleration when available",
"bufferSize": "Buffer Size",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Customize subtitle appearance",
"smallSkipDuration": "Small Skip Duration",
"largeSkipDuration": "Large Skip Duration",
"rewindOnResume": "Rewind on Resume",
"secondsUnit": "${seconds} seconds",
"defaultSleepTimer": "Default Sleep Timer",
"minutesUnit": "${minutes} minutes",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Customize keyboard shortcuts",
"videoPlayerNavigation": "Video Player Navigation",
"videoPlayerNavigationDescription": "Use arrow keys to navigate video player controls",
"watchTogetherRelay": "Watch Together Relay",
"watchTogetherRelayDescription": "Set a custom relay server for Watch Together. All participants must use the same server.",
"watchTogetherRelayHint": "https://my-relay.example.com",
"crashReporting": "Crash Reporting",
"crashReportingDescription": "Send crash reports to help improve the app",
"debugLogging": "Debug Logging",
"debugLoggingDescription": "Enable detailed logging for troubleshooting",
"viewLogs": "View Logs",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Automatically skip credits and play next episode",
"autoSkipDelay": "Auto Skip Delay",
"autoSkipDelayDescription": "Wait ${seconds} seconds before auto-skipping",
"introPattern": "Intro Marker Pattern",
"introPatternDescription": "Regex pattern to match intro markers in chapter titles",
"creditsPattern": "Credits Marker Pattern",
"creditsPatternDescription": "Regex pattern to match credits markers in chapter titles",
"invalidRegex": "Invalid regular expression",
"downloads": "Downloads",
"downloadLocationDescription": "Choose where to store downloaded content",
"downloadLocationDefault": "Default (App Storage)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Failed to select folder",
"downloadOnWifiOnly": "Download on WiFi only",
"downloadOnWifiOnlyDescription": "Prevent downloads when on cellular data",
"autoRemoveWatchedDownloads": "Auto-remove watched downloads",
"autoRemoveWatchedDownloadsDescription": "Automatically delete downloaded episodes and movies when marked as watched",
"cellularDownloadBlocked": "Downloads are disabled on cellular data. Connect to WiFi or change the setting.",
"maxVolume": "Maximum Volume",
"maxVolumeDescription": "Allow volume boost above 100% for quiet media",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Show what you're watching on Discord",
"trakt": "Trakt",
"traktDescription": "Sync watch history with Trakt",
"companionRemoteServer": "Companion Remote Server",
"companionRemoteServerDescription": "Allow mobile devices on your network to control this app",
"autoPip": "Auto Picture-in-Picture",
"autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback",
"matchContentFrameRate": "Match Content Frame Rate",
"matchContentFrameRateDescription": "Adjust display refresh rate to match video content, reducing judder and saving battery",
"matchRefreshRate": "Match Refresh Rate",
"matchRefreshRateDescription": "Switch display refresh rate to match video content when in fullscreen",
"matchDynamicRange": "Match Dynamic Range",
"matchDynamicRangeDescription": "Auto-enable HDR for HDR content and revert to SDR when leaving the player",
"displaySwitchDelay": "Display Switch Delay",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"requireProfileSelectionOnOpen": "Ask for profile on app open",
"requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened",
"confirmExitOnBack": "Confirm before exiting",
"confirmExitOnBackDescription": "Show a confirmation dialog when pressing back to exit the app"
"confirmExitOnBackDescription": "Show a confirmation dialog when pressing back to exit the app",
"forceTvMode": "Force TV mode",
"forceTvModeDescription": "Use the TV layout regardless of auto-detection. Useful on Android TV devices that don't report the leanback feature. Restarts the app on change.",
"autoHidePerformanceOverlay": "Auto-Hide Performance Overlay",
"autoHidePerformanceOverlayDescription": "Fade the performance overlay with the playback controls",
"showNavBarLabels": "Show Navigation Bar Labels",
"showNavBarLabelsDescription": "Display text labels under navigation bar icons",
"liveTvDefaultFavorites": "Default to Favorite Channels",
"liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Search movies, shows, music...",
@@ -239,11 +263,6 @@
"skipMarker": "Skip Intro/Credits"
}
},
"pinEntry": {
"enterPin": "Enter PIN",
"showPin": "Show PIN",
"hidePin": "Hide PIN"
},
"fileInfo": {
"title": "File Info",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Color Primaries",
"chromaSubsampling": "Chroma Subsampling",
"channels": "Channels",
"subtitles": "Subtitles",
"overallBitrate": "Overall Bitrate",
"path": "Path",
"size": "Size",
"container": "Container",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "This includes all episodes and their files.",
"mediaDeletedSuccessfully": "Media item deleted successfully",
"mediaFailedToDelete": "Failed to delete media item",
"rate": "Rate"
"rate": "Rate",
"playFromBeginning": "Play from Beginning",
"playVersion": "Play Version..."
},
"accessibility": {
"mediaCardMovie": "${title}, movie",
@@ -316,7 +339,6 @@
"unlockRotation": "Unlock rotation",
"timerActive": "Timer Active",
"playbackWillPauseIn": "Playback will pause in ${duration}",
"sleepTimerCompleted": "Sleep timer completed - playback paused",
"stillWatching": "Still watching?",
"pausingIn": "Pausing in ${seconds}s",
"continueWatching": "Continue",
@@ -333,34 +355,43 @@
"muteButton": "Mute",
"unmuteButton": "Unmute",
"settingsButton": "Video settings",
"audioTrackButton": "Audio tracks",
"subtitlesButton": "Subtitles",
"tracksButton": "Audio & Subtitles",
"chaptersButton": "Chapters",
"versionsButton": "Video versions",
"pipButton": "Picture-in-Picture mode",
"aspectRatioButton": "Aspect ratio",
"ambientLighting": "Ambient lighting",
"ambientLightingOn": "Enable ambient lighting",
"ambientLightingOff": "Disable ambient lighting",
"fullscreenButton": "Enter fullscreen",
"exitFullscreenButton": "Exit fullscreen",
"alwaysOnTopButton": "Always on top",
"rotationLockButton": "Rotation lock",
"lockScreen": "Lock screen",
"screenLockButton": "Screen lock",
"longPressToUnlock": "Long press to unlock",
"timelineSlider": "Video timeline",
"volumeSlider": "Volume level",
"endsAt": "Ends at ${time}",
"pipActive": "Playing in Picture-in-Picture",
"pipFailed": "Picture-in-picture failed to start",
"pipErrors": {
"androidVersion": "Requires Android 8.0 or newer",
"iosVersion": "Requires iOS 15.0 or newer",
"permissionDisabled": "Picture-in-picture permission is disabled. Enable it in Settings > Apps > Plezy > Picture-in-picture",
"notSupported": "Device doesn't support picture-in-picture mode",
"voSwitchFailed": "Failed to switch video output for picture-in-picture",
"failed": "Picture-in-picture failed to start",
"unknown": "An error occurred: ${error}"
},
"chapters": "Chapters",
"noChaptersAvailable": "No chapters available",
"queue": "Queue",
"noQueueItems": "No items in queue"
"noQueueItems": "No items in queue",
"searchSubtitles": "Search Subtitles",
"language": "Language",
"noSubtitlesFound": "No subtitles found",
"subtitleDownloaded": "Subtitle downloaded",
"subtitleDownloadFailed": "Failed to download subtitle",
"searchLanguages": "Search languages..."
},
"userStatus": {
"admin": "Admin",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Marked as unwatched",
"markedAsWatchedOffline": "Marked as watched (will sync when online)",
"markedAsUnwatchedOffline": "Marked as unwatched (will sync when online)",
"autoRemovedWatchedDownload": "Auto-removed: ${title}",
"removedFromContinueWatching": "Removed from Continue Watching",
"errorLoading": "Error: ${error}",
"fileInfoNotAvailable": "File information not available",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Failed to create play queue - no items",
"failedPlayback": "Failed to ${action}: ${error}",
"switchingToCompatiblePlayer": "Switching to compatible player...",
"serverLimitTitle": "Playback failed",
"serverLimitBody": "The server returned an error (HTTP 500). This usually means the server owner has set a bandwidth or transcoding limit that's rejecting your session. There's nothing to do from the client — the server owner needs to adjust their settings.",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
},
"subtitlingStyling": {
"stylingOptions": "Styling Options",
"text": "Text",
"border": "Border",
"background": "Background",
"fontSize": "Font Size",
"textColor": "Text Color",
"borderSize": "Border Size",
"borderColor": "Border Color",
"backgroundOpacity": "Background Opacity",
"backgroundColor": "Background Color",
"position": "Position"
"position": "Position",
"assOverride": "ASS Override",
"bold": "Bold",
"italic": "Italic"
},
"mpvConfig": {
"title": "MPV Configuration",
"title": "mpv.conf",
"description": "Advanced video player settings",
"properties": "Properties",
"presets": "Presets",
"noProperties": "No properties configured",
"noPresets": "No saved presets",
"addProperty": "Add Property",
"editProperty": "Edit Property",
"deleteProperty": "Delete Property",
"propertyKey": "Property Key",
"propertyKeyHint": "e.g., hwdec, demuxer-max-bytes",
"propertyValue": "Property Value",
"propertyValueHint": "e.g., auto, 256000000",
"saveAsPreset": "Save as Preset...",
"presetName": "Preset Name",
"presetNameHint": "Enter a name for this preset",
@@ -436,8 +466,7 @@
"presetLoaded": "Preset loaded",
"presetDeleted": "Preset deleted",
"confirmDeletePreset": "Are you sure you want to delete this preset?",
"confirmDeleteProperty": "Are you sure you want to delete this property?",
"entriesCount": "${count} entries"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Confirm Action"
@@ -452,11 +481,8 @@
"overview": "Overview",
"cast": "Cast",
"extras": "Trailers & Extras",
"seasons": "Seasons",
"studio": "Studio",
"rating": "Rating",
"episodeCount": "${count} episodes",
"watchedProgress": "${watched}/${total} watched",
"movie": "Movie",
"tvShow": "TV Show",
"minutesLeft": "${minutes} min left"
@@ -518,6 +544,7 @@
"playlists": "Playlists"
},
"groupings": {
"title": "Grouping",
"all": "All",
"movies": "Movies",
"shows": "TV Shows",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Clear Logs",
"copyLogs": "Copy Logs",
"uploadLogs": "Upload Logs",
"error": "Error:",
"stackTrace": "Stack Trace:"
"uploadLogs": "Upload Logs"
},
"licenses": {
"relatedPackages": "Related Packages",
@@ -565,20 +590,12 @@
},
"liveTv": {
"title": "Live TV",
"channels": "Channels",
"guide": "Guide",
"noChannels": "No channels available",
"noDvr": "No DVR configured on any server",
"tuneFailed": "Failed to tune channel",
"loading": "Loading channels...",
"nowPlaying": "Now Playing",
"noPrograms": "No program data available",
"channelNumber": "Ch. ${number}",
"live": "LIVE",
"hd": "HD",
"premiere": "NEW",
"reloadGuide": "Reload Guide",
"allChannels": "All Channels",
"now": "Now",
"today": "Today",
"midnight": "Midnight",
@@ -588,7 +605,13 @@
"evening": "Evening",
"lateNight": "Late Night",
"whatsOn": "What's On",
"watchChannel": "Watch Channel"
"watchChannel": "Watch Channel",
"favorites": "Favorites",
"reorderFavorites": "Reorder Favorites",
"joinSession": "Join Session in Progress",
"watchFromStart": "Watch from start (${minutes} min ago)",
"watchLive": "Watch Live",
"goToLive": "Go to Live"
},
"collections": {
"title": "Collections",
@@ -602,7 +625,6 @@
"deleteFailedWithError": "Failed to delete collection: ${error}",
"failedToLoadItems": "Failed to load collection items: ${error}",
"selectCollection": "Select Collection",
"createNewCollection": "Create New Collection",
"collectionName": "Collection Name",
"enterCollectionName": "Enter collection name",
"addedToCollection": "Added to collection",
@@ -612,7 +634,8 @@
"removeFromCollectionConfirm": "Remove \"${title}\" from this collection?",
"removedFromCollection": "Removed from collection",
"removeFromCollectionFailed": "Failed to remove from collection",
"removeFromCollectionError": "Error removing from collection: ${error}"
"removeFromCollectionError": "Error removing from collection: ${error}",
"searchCollections": "Search collections..."
},
"playlists": {
"title": "Playlists",
@@ -634,7 +657,6 @@
"itemAdded": "Added to playlist",
"itemRemoved": "Removed from playlist",
"selectPlaylist": "Select Playlist",
"createNewPlaylist": "Create New Playlist",
"errorCreating": "Failed to create playlist",
"errorDeleting": "Failed to delete playlist",
"errorLoading": "Failed to load playlists",
@@ -677,18 +699,30 @@
"leave": "Leave",
"syncing": "Syncing...",
"joinWatchSession": "Join Watch Session",
"enterCodeHint": "Enter 8-character code",
"enterCodeHint": "Enter 5-character code",
"pasteFromClipboard": "Paste from clipboard",
"pleaseEnterCode": "Please enter a session code",
"codeMustBe8Chars": "Session code must be 8 characters",
"codeMustBe5Chars": "Session code must be 5 characters",
"joinInstructions": "Enter the session code shared by the host to join their watch session.",
"failedToCreate": "Failed to create session",
"failedToJoin": "Failed to join session",
"sessionCodeCopied": "Session code copied to clipboard",
"relayUnreachable": "The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.",
"reconnectingToHost": "Reconnecting to host...",
"currentPlayback": "Current Playback",
"joinCurrentPlayback": "Join Current Playback",
"joinCurrentPlaybackDescription": "Jump back into what the host is currently watching",
"failedToOpenCurrentPlayback": "Failed to open current playback",
"participantJoined": "${name} joined",
"participantLeft": "${name} left"
"participantLeft": "${name} left",
"participantPaused": "${name} paused",
"participantResumed": "${name} resumed",
"participantSeeked": "${name} seeked",
"participantBuffering": "${name} is buffering",
"waitingForParticipants": "Waiting for others to load...",
"recentRooms": "Recent Rooms",
"renameRoom": "Rename Room",
"removeRoom": "Remove"
},
"downloads": {
"title": "Downloads",
@@ -701,6 +735,7 @@
"deleteDownload": "Delete download",
"retryDownload": "Retry download",
"downloadQueued": "Download queued",
"serverErrorBitrate": "Server error — the file may exceed the remote streaming bitrate limit",
"episodesQueued": "${count} episodes queued for download",
"downloadDeleted": "Download deleted",
"deleteConfirm": "Are you sure you want to delete \"${title}\"? This will remove the downloaded file from your device.",
@@ -708,7 +743,32 @@
"noDownloadsTree": "No downloads",
"pauseAll": "Pause all",
"resumeAll": "Resume all",
"deleteAll": "Delete all"
"deleteAll": "Delete all",
"selectVersion": "Select Version",
"allEpisodes": "All episodes",
"unwatchedOnly": "Unwatched only",
"nextNUnwatched": "Next ${count} unwatched",
"customAmount": "Custom amount...",
"howManyEpisodes": "How many episodes?",
"itemsQueued": "${count} items queued for download",
"keepSynced": "Keep synced",
"downloadOnce": "Download once",
"keepNUnwatched": "Keep ${count} unwatched",
"editSyncRule": "Edit sync rule",
"removeSyncRule": "Remove sync rule",
"removeSyncRuleConfirm": "Stop syncing \"${title}\"? Downloaded episodes will be kept.",
"syncRuleCreated": "Sync rule created — keeping ${count} unwatched episodes",
"syncRuleUpdated": "Sync rule updated",
"syncRuleRemoved": "Sync rule removed",
"syncedNewEpisodes": "Synced ${count} new episodes for ${title}",
"activeSyncRules": "Sync rules",
"noSyncRules": "No sync rules",
"manageSyncRule": "Manage sync",
"editEpisodeCount": "Episode count",
"editSyncFilter": "Sync filter",
"syncAllItems": "Syncing all items",
"syncUnwatchedItems": "Syncing unwatched items",
"syncRuleListCreated": "Sync rule created"
},
"shaders": {
"title": "Shaders",
@@ -716,58 +776,46 @@
"nvscalerDescription": "NVIDIA image scaling for sharper video",
"qualityFast": "Fast",
"qualityHQ": "High Quality",
"mode": "Mode"
"mode": "Mode",
"importShader": "Import Shader",
"customShaderDescription": "Custom GLSL shader",
"shaderImported": "Shader imported",
"shaderImportFailed": "Failed to import shader",
"deleteShader": "Delete Shader",
"deleteShaderConfirm": "Delete \"${name}\"?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Connect to Device",
"hostRemoteSession": "Host Remote Session",
"controlThisDevice": "Control this device with your phone",
"remoteControl": "Remote Control",
"controlDesktop": "Control a desktop device",
"connectedTo": "Connected to ${name}",
"session": {
"creatingSession": "Creating remote session...",
"failedToCreate": "Failed to create remote session:",
"noSession": "No session available",
"scanQrCode": "Scan QR Code",
"orEnterManually": "Or enter manually",
"startingServer": "Starting remote server...",
"failedToCreate": "Failed to start remote server:",
"hostAddress": "Host Address",
"sessionId": "Session ID",
"pin": "PIN",
"connected": "Connected",
"waitingForConnection": "Waiting for connection...",
"serverRunning": "Remote server active",
"serverStopped": "Remote server stopped",
"serverRunningDescription": "Mobile devices on your network can discover and connect to this app",
"serverStoppedDescription": "Start the server to allow mobile devices to connect",
"usePhoneToControl": "Use your mobile device to control this app",
"copiedToClipboard": "${label} copied to clipboard",
"copyToClipboard": "Copy to clipboard",
"newSession": "New Session",
"startServer": "Start Server",
"stopServer": "Stop Server",
"minimize": "Minimize"
},
"pairing": {
"scan": "Scan",
"manual": "Manual",
"pairWithDesktop": "Pair with Desktop",
"enterSessionDetails": "Enter the session details shown on your desktop device",
"discoveryDescription": "Devices on your network running Plezy with the same Plex account will appear automatically",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Enter 8-character session ID",
"pinHint": "Enter 6-digit PIN",
"connecting": "Connecting...",
"tips": "Tips",
"tipDesktop": "Open Plezy on your desktop and enable Companion Remote from settings or menu",
"tipScan": "Use the Scan tab to quickly pair by scanning the QR code on your desktop",
"tipWifi": "Make sure both devices are on the same WiFi network",
"cameraPermissionRequired": "Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.",
"cameraError": "Could not start camera: ${error}",
"scanInstruction": "Point your camera at the QR code shown on your desktop",
"invalidQrCode": "Invalid QR code format",
"searchingForDevices": "Looking for devices...",
"noDevicesFound": "No devices found on your network",
"noDevicesHint": "Make sure Plezy is open on your desktop and both devices are on the same WiFi network",
"availableDevices": "Available Devices",
"manualConnection": "Manual Connection",
"cryptoInitFailed": "Could not initialize secure connection. Make sure you are signed in to a Plex account.",
"validationHostRequired": "Please enter host address",
"validationHostFormat": "Format must be IP:port (e.g., 192.168.1.100:48632)",
"validationSessionIdRequired": "Please enter a session ID",
"validationSessionIdLength": "Session ID must be 8 characters",
"validationPinRequired": "Please enter a PIN",
"validationPinLength": "PIN must be 6 digits",
"connectionTimedOut": "Connection timed out. Please check the session ID and PIN.",
"sessionNotFound": "Could not find the session. Please check your credentials.",
"connectionTimedOut": "Connection timed out. Make sure both devices are on the same network.",
"sessionNotFound": "Could not find the device. Make sure Plezy is running on the host.",
"authFailed": "Authentication failed. Make sure both devices are on the same Plex account.",
"failedToConnect": "Failed to connect: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Reconnecting...",
"attemptOf": "Attempt ${current} of 5",
"retryNow": "Retry Now",
"connectionError": "Connection error",
"notConnected": "Not connected",
"tabRemote": "Remote",
"tabPlay": "Play",
"tabMore": "More",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Use External Player",
"useExternalPlayerDescription": "Open videos in an external app instead of the built-in player",
"selectPlayer": "Select Player",
"customPlayers": "Custom Players",
"systemDefault": "System Default",
"addCustomPlayer": "Add Custom Player",
"playerName": "Player Name",
"playerCommand": "Command",
"playerPackage": "Package Name",
"playerUrlScheme": "URL Scheme",
"customPlayer": "Custom Player",
"off": "Off",
"launchFailed": "Failed to open external player",
"appNotInstalled": "${name} is not installed",
@@ -847,8 +893,12 @@
"summary": "Summary",
"poster": "Poster",
"background": "Background",
"logo": "Logo",
"squareArt": "Square Art",
"selectPoster": "Select Poster",
"selectBackground": "Select Background",
"selectLogo": "Select Logo",
"selectSquareArt": "Select Square Art",
"fromUrl": "From URL",
"uploadFile": "Upload File",
"enterImageUrl": "Enter image URL",
@@ -890,6 +940,39 @@
"subtitleMode": "Auto-Select Subtitle Mode",
"manuallySelected": "Manually selected",
"shownWithForeignAudio": "Shown with foreign audio",
"alwaysEnabled": "Always enabled"
"alwaysEnabled": "Always enabled",
"tags": "Tags",
"addTag": "Add tag",
"genre": "Genre",
"director": "Director",
"writer": "Writer",
"producer": "Producer",
"country": "Country",
"collection": "Collection",
"label": "Label",
"style": "Style",
"mood": "Mood"
},
"serverTasks": {
"title": "Server Tasks",
"failedToLoad": "Failed to load tasks",
"noTasks": "No tasks running"
},
"trakt": {
"title": "Trakt",
"connected": "Connected",
"connectedAs": "Connected as @${username}",
"disconnectConfirm": "Disconnect Trakt account?",
"disconnectConfirmBody": "Plezy will stop sending playback events to Trakt. You can reconnect at any time.",
"scrobble": "Real-time scrobbling",
"scrobbleDescription": "Send play, pause, and stop events to Trakt during playback.",
"watchedSync": "Sync watched status",
"watchedSyncDescription": "When you mark items watched in Plezy, mark them on Trakt.",
"deviceCodeTitle": "Activate Plezy on Trakt",
"deviceCodeBody": "Visit ${url} and enter this code:",
"openTraktActivate": "Open Trakt to activate",
"waitingForAuthorization": "Waiting for authorization…",
"codeCopied": "Code copied",
"connectFailed": "Couldn't connect to Trakt. Try again."
}
}
+207 -124
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Inicia sesión con Plex",
"showQRCode": "Mostrar código QR",
"authenticate": "Autenticar",
"debugEnterToken": "Debug: Ingresar Token de Plex",
"plexTokenLabel": "Token de Autenticación de Plex",
"plexTokenHint": "Ingresa tu token de Plex.tv",
"authenticationTimeout": "Tiempo de autenticación agotado. Por favor, intenta de nuevo.",
"scanQRToSignIn": "Escanea este código QR para iniciar sesión",
"waitingForAuth": "Esperando autenticación...\nPor favor completa el inicio de sesión en tu navegador.",
@@ -32,8 +29,7 @@
"delete": "Eliminar",
"shuffle": "Aleatorio",
"addTo": "Añadir a...",
"remove": "Eliminar",
"paste": "Pegar",
"createNew": "Crear",
"connect": "Conectar",
"disconnect": "Desconectar",
"play": "Reproducir",
@@ -46,7 +42,6 @@
"settings": "Ajustes",
"mute": "Silencio",
"ok": "OK",
"loading": "Cargando...",
"reconnect": "Reconectar",
"exitConfirmTitle": "¿Salir de la app?",
"exitConfirmMessage": "¿Estás seguro de que quieres salir?",
@@ -57,13 +52,14 @@
"refreshingServers": "Actualizando servidores...",
"loadingServers": "Cargando servidores...",
"connectingToServers": "Conectando a servidores...",
"startingOfflineMode": "Iniciando modo sin conexión..."
"startingOfflineMode": "Iniciando modo sin conexión...",
"loading": "Cargando..."
},
"screens": {
"licenses": "Licencias",
"switchProfile": "Cambiar Perfil",
"subtitleStyling": "Estilo de Subtítulos",
"mpvConfig": "Configuración de MPV",
"mpvConfig": "mpv.conf",
"logs": "Logs"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Configuración",
"supportDeveloper": "Apoya Plezy",
"supportDeveloperDescription": "Dona vía Liberapay para financiar el desarrollo",
"language": "Idioma",
"theme": "Tema",
"appearance": "Apariencia",
"videoPlayback": "Reproducción de Video",
"videoPlaybackDescription": "Configurar el comportamiento de reproducción",
"advanced": "Avanzado",
"episodePosterMode": "Estilo de Póster de Episodio",
"seriesPoster": "Póster de Serie",
"seriesPosterDescription": "Mostrar el póster de la serie para todos los episodios",
"seasonPoster": "Póster de Temporada",
"seasonPosterDescription": "Mostrar el póster de la temporada para los episodios",
"episodeThumbnail": "Miniatura de Episodio",
"episodeThumbnailDescription": "Mostrar miniaturas de capturas de pantalla de episodios en 16:9",
"episodeThumbnail": "Miniatura",
"showHeroSectionDescription": "Mostrar carrusel de contenido destacado en la pantalla de inicio",
"secondsLabel": "Segundos",
"minutesLabel": "Minutos",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Ingresa la duración (${min}-${max})",
"systemTheme": "Sistema",
"systemThemeDescription": "Sigue la configuración del sistema",
"lightTheme": "Claro",
"darkTheme": "Oscuro",
"oledTheme": "OLED",
"oledThemeDescription": "Negro puro para pantallas OLED",
"libraryDensity": "Densidad de Biblioteca",
"compact": "Compacto",
"compactDescription": "Tarjetas más pequeñas, más elementos visibles",
"normal": "Normal",
"normalDescription": "Tamaño predeterminado",
"comfortable": "Cómodo",
"comfortableDescription": "Tarjetas más grandes, menos elementos visibles",
"viewMode": "Modo de Vista",
"gridView": "Cuadrícula",
"gridViewDescription": "Mostrar elementos en un diseño de cuadrícula",
"listView": "Lista",
"listViewDescription": "Mostrar elementos en un diseño de lista",
"showHeroSection": "Mostrar Sección Destacada",
"useGlobalHubs": "Usar Diseño de Inicio de Plex",
"useGlobalHubsDescription": "Mostrar los hubs de la página de inicio como el cliente oficial de Plex. Cuando está desactivado, muestra recomendaciones por biblioteca en su lugar.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Difuminar miniaturas y ocultar descripciones de episodios que aún no has visto",
"playerBackend": "Reproductor",
"exoPlayer": "ExoPlayer (Recomendado)",
"exoPlayerDescription": "Reproductor nativo de Android con mejor soporte de hardware",
"mpv": "MPV",
"mpvDescription": "Reproductor avanzado con más funciones y soporte de subtítulos ASS",
"mpv": "mpv",
"hardwareDecoding": "Decodificación por Hardware",
"hardwareDecodingDescription": "Usar aceleración por hardware cuando esté disponible",
"bufferSize": "Tamaño del Buffer",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Personalizar la apariencia de los subtítulos",
"smallSkipDuration": "Salto pequeño",
"largeSkipDuration": "Salto grande",
"rewindOnResume": "Rebobinar al reanudar",
"secondsUnit": "${seconds} segundos",
"defaultSleepTimer": "Temporizador de apagado",
"minutesUnit": "${minutes} minutos",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Personalizar los atajos de teclado",
"videoPlayerNavigation": "Navegación del Reproductor de Video",
"videoPlayerNavigationDescription": "Usar las teclas de flecha para navegar por los controles del reproductor",
"watchTogetherRelay": "Relay de Ver Juntos",
"watchTogetherRelayDescription": "Configurar un servidor relay personalizado para Ver Juntos. Todos los participantes deben usar el mismo servidor.",
"watchTogetherRelayHint": "https://mi-relay.ejemplo.com",
"crashReporting": "Informes de Errores",
"crashReportingDescription": "Enviar informes de errores para mejorar la aplicación",
"debugLogging": "Registro de Depuración",
"debugLoggingDescription": "Habilitar registros detallados para resolución de problemas",
"viewLogs": "Ver Logs",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Saltar automáticamente los créditos y reproducir el siguiente episodio",
"autoSkipDelay": "Retraso de Salto automático",
"autoSkipDelayDescription": "Esperar ${seconds} segundos antes de saltar automáticamente",
"introPattern": "Patrón de marcador de intro",
"introPatternDescription": "Expresión regular para reconocer marcadores de intro en los títulos de capítulos",
"creditsPattern": "Patrón de marcador de créditos",
"creditsPatternDescription": "Expresión regular para reconocer marcadores de créditos en los títulos de capítulos",
"invalidRegex": "Expresión regular no válida",
"downloads": "Descargas",
"downloadLocationDescription": "Elegir dónde almacenar el contenido descargado",
"downloadLocationDefault": "Predeterminado (Almacenamiento de la App)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Error al seleccionar la carpeta",
"downloadOnWifiOnly": "Descargar solo con WiFi",
"downloadOnWifiOnlyDescription": "Evitar descargas cuando se usan datos móviles",
"autoRemoveWatchedDownloads": "Eliminar descargas vistas automáticamente",
"autoRemoveWatchedDownloadsDescription": "Eliminar automáticamente episodios y películas descargados cuando se marquen como vistos",
"cellularDownloadBlocked": "Las descargas están desactivadas en datos móviles. Conéctate a una red WiFi o cambia la configuración.",
"maxVolume": "Volumen Máximo",
"maxVolumeDescription": "Permitir aumento de volumen por encima del 100% para medios con sonido bajo",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Presencia de Discord",
"discordRichPresenceDescription": "Mostrar lo que estás viendo en Discord",
"trakt": "Trakt",
"traktDescription": "Sincronizar historial de visualización con Trakt",
"companionRemoteServer": "Servidor de control remoto",
"companionRemoteServerDescription": "Permitir que dispositivos móviles en tu red controlen esta aplicación",
"autoPip": "Imagen en imagen automática",
"autoPipDescription": "Activar automáticamente imagen en imagen al salir de la app durante la reproducción",
"matchContentFrameRate": "Ajustar frecuencia de actualización",
"matchContentFrameRateDescription": "Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Ajustar frecuencia de refresco",
"matchRefreshRateDescription": "Cambiar la frecuencia de refresco de la pantalla para coincidir con el contenido de video en pantalla completa",
"matchDynamicRange": "Ajustar rango dinámico",
"matchDynamicRangeDescription": "Activar HDR automáticamente para contenido HDR y volver a SDR al salir del reproductor",
"displaySwitchDelay": "Retraso de cambio de pantalla",
"tunneledPlayback": "Reproducción tunelizada",
"tunneledPlaybackDescription": "Usar tunelización de video acelerada por hardware. Desactivar si ves una pantalla negra con audio en contenido HDR",
"requireProfileSelectionOnOpen": "Pedir perfil al abrir la app",
"requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación",
"confirmExitOnBack": "Confirmar antes de salir",
"confirmExitOnBackDescription": "Mostrar un diálogo de confirmación al presionar atrás para salir de la app"
"confirmExitOnBackDescription": "Mostrar un diálogo de confirmación al presionar atrás para salir de la app",
"forceTvMode": "Forzar modo TV",
"forceTvModeDescription": "Usar el diseño de TV independientemente de la detección automática. Útil en dispositivos Android TV que no informan la característica leanback. Reinicia la app al cambiar.",
"autoHidePerformanceOverlay": "Ocultar superposición de rendimiento automáticamente",
"autoHidePerformanceOverlayDescription": "Desvanecer la superposición de rendimiento con los controles de reproducción",
"showNavBarLabels": "Mostrar etiquetas de la barra de navegación",
"showNavBarLabelsDescription": "Mostrar etiquetas de texto bajo los iconos de la barra de navegación",
"liveTvDefaultFavorites": "Canales favoritos por defecto",
"liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Buscar películas, series, música...",
@@ -239,11 +263,6 @@
"skipMarker": "Saltar Intro/Créditos"
}
},
"pinEntry": {
"enterPin": "Introduce el PIN",
"showPin": "Mostrar PIN",
"hidePin": "Ocultar PIN"
},
"fileInfo": {
"title": "Información del Archivo",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Primarias de color",
"chromaSubsampling": "Submuestreo de croma",
"channels": "Canales",
"subtitles": "Subtítulos",
"overallBitrate": "Bitrate total",
"path": "Ruta",
"size": "Tamaño",
"container": "Contenedor",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Esto incluye todos los episodios y sus archivos.",
"mediaDeletedSuccessfully": "Elemento multimedia eliminado con éxito",
"mediaFailedToDelete": "Error al eliminar el elemento multimedia",
"rate": "Calificar"
"rate": "Calificar",
"playFromBeginning": "Reproducir desde el inicio",
"playVersion": "Reproducir versión..."
},
"accessibility": {
"mediaCardMovie": "${title}, película",
@@ -316,7 +339,6 @@
"unlockRotation": "Desbloquear rotación",
"timerActive": "Temporizador Activo",
"playbackWillPauseIn": "La reproducción se pausará en ${duration}",
"sleepTimerCompleted": "Temporizador completado - reproducción pausada",
"stillWatching": "¿Sigues viendo?",
"pausingIn": "Pausa en ${seconds}s",
"continueWatching": "Continuar",
@@ -333,34 +355,43 @@
"muteButton": "Silenciar",
"unmuteButton": "Activar sonido",
"settingsButton": "Ajustes de video",
"audioTrackButton": "Pistas de audio",
"subtitlesButton": "Subtítulos",
"tracksButton": "Audio y subtítulos",
"chaptersButton": "Capítulos",
"versionsButton": "Versiones de video",
"pipButton": "Modo PiP (Imagen en Imagen)",
"aspectRatioButton": "Relación de aspecto",
"ambientLighting": "Iluminación ambiental",
"ambientLightingOn": "Activar iluminación ambiental",
"ambientLightingOff": "Desactivar iluminación ambiental",
"fullscreenButton": "Entrar en pantalla completa",
"exitFullscreenButton": "Salir de pantalla completa",
"alwaysOnTopButton": "Siempre visible",
"rotationLockButton": "Bloqueo de rotación",
"lockScreen": "Bloquear pantalla",
"screenLockButton": "Bloqueo de pantalla",
"longPressToUnlock": "Mantén pulsado para desbloquear",
"timelineSlider": "Línea de tiempo del video",
"volumeSlider": "Nivel de volumen",
"endsAt": "Termina a las ${time}",
"pipActive": "Reproduciendo en Imagen en Imagen",
"pipFailed": "Error al iniciar Imagen en Imagen",
"pipErrors": {
"androidVersion": "Requiere Android 8.0 o más reciente",
"iosVersion": "Requiere iOS 15.0 o más reciente",
"permissionDisabled": "El permiso de Imagen en Imagen está desactivado. Actívalo en Ajustes > Aplicaciones > Plezy > Imagen en Imagen",
"notSupported": "El dispositivo no soporta el modo Imagen en Imagen",
"voSwitchFailed": "Error al cambiar la salida de video para Imagen en Imagen",
"failed": "Error al iniciar Imagen en Imagen",
"unknown": "Ocurrió un error: ${error}"
},
"chapters": "Capítulos",
"noChaptersAvailable": "No hay capítulos disponibles",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "Cola",
"noQueueItems": "No hay elementos en la cola",
"searchSubtitles": "Buscar subtítulos",
"language": "Idioma",
"noSubtitlesFound": "No se encontraron subtítulos",
"subtitleDownloaded": "Subtítulo descargado",
"subtitleDownloadFailed": "Error al descargar subtítulo",
"searchLanguages": "Buscar idiomas..."
},
"userStatus": {
"admin": "Administrador",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Marcado como no visto",
"markedAsWatchedOffline": "Marcado como visto (se sincronizará al estar en línea)",
"markedAsUnwatchedOffline": "Marcado como no visto (se sincronizará al estar en línea)",
"autoRemovedWatchedDownload": "Eliminado automáticamente: ${title}",
"removedFromContinueWatching": "Eliminado de Seguir Viendo",
"errorLoading": "Error: ${error}",
"fileInfoNotAvailable": "Información de archivo no disponible",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Error al crear la cola de reproducción - no hay elementos",
"failedPlayback": "Error al ${action}: ${error}",
"switchingToCompatiblePlayer": "Cambiando a reproductor compatible...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Error de reproducción",
"serverLimitBody": "El servidor devolvió un error (HTTP 500). Normalmente esto significa que el propietario del servidor ha establecido un límite de ancho de banda o transcodificación que está rechazando tu sesión. No hay nada que puedas hacer desde el cliente — el propietario del servidor debe ajustar su configuración.",
"logsUploaded": "Registros subidos",
"logsUploadFailed": "Error al subir registros",
"logId": "ID de registro"
},
"subtitlingStyling": {
"stylingOptions": "Opciones de Estilo",
"text": "Texto",
"border": "Borde",
"background": "Fondo",
"fontSize": "Tamaño de Fuente",
"textColor": "Color de Texto",
"borderSize": "Tamaño de Borde",
"borderColor": "Color de Borde",
"backgroundOpacity": "Opacidad de Fondo",
"backgroundColor": "Color de Fondo",
"position": "Position"
"position": "Posición",
"assOverride": "Sobreescritura ASS",
"bold": "Negrita",
"italic": "Cursiva"
},
"mpvConfig": {
"title": "Configuración de MPV",
"title": "Configuración de mpv",
"description": "Ajustes avanzados del reproductor de video",
"properties": "Propiedades",
"presets": "Ajustes preestablecidos",
"noProperties": "No hay propiedades configuradas",
"noPresets": "No hay ajustes guardados",
"addProperty": "Añadir Propiedad",
"editProperty": "Editar Propiedad",
"deleteProperty": "Eliminar Propiedad",
"propertyKey": "Clave de la Propiedad",
"propertyKeyHint": "ej., hwdec, demuxer-max-bytes",
"propertyValue": "Valor de la Propiedad",
"propertyValueHint": "ej., auto, 256000000",
"saveAsPreset": "Guardar como Ajuste...",
"presetName": "Nombre del Ajuste",
"presetNameHint": "Introduce un nombre para este ajuste",
@@ -436,8 +466,7 @@
"presetLoaded": "Ajuste cargado",
"presetDeleted": "Ajuste eliminado",
"confirmDeletePreset": "¿Estás seguro de que quieres eliminar este ajuste?",
"confirmDeleteProperty": "¿Estás seguro de que quieres eliminar esta propiedad?",
"entriesCount": "${count} entradas"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Confirmar Acción"
@@ -452,11 +481,8 @@
"overview": "Resumen",
"cast": "Reparto",
"extras": "Tráilers y Extras",
"seasons": "Temporadas",
"studio": "Estudio",
"rating": "Calificación",
"episodeCount": "${count} episodios",
"watchedProgress": "${watched}/${total} vistos",
"movie": "Película",
"tvShow": "Serie de TV",
"minutesLeft": "quedan ${minutes} min"
@@ -518,6 +544,7 @@
"playlists": "Listas"
},
"groupings": {
"title": "Agrupación",
"all": "Todo",
"movies": "Películas",
"shows": "Series",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Borrar Logs",
"copyLogs": "Copiar Logs",
"uploadLogs": "Upload Logs",
"error": "Error:",
"stackTrace": "Traza de la pila (Stack Trace):"
"uploadLogs": "Subir registros"
},
"licenses": {
"relatedPackages": "Paquetes relacionados",
@@ -559,26 +584,18 @@
"licensesCount": "${count} licencias"
},
"navigation": {
"libraries": "Bibliotecas",
"libraries": "Medios",
"downloads": "Descargas",
"liveTv": "TV en vivo"
},
"liveTv": {
"title": "TV en vivo",
"channels": "Canales",
"guide": "Guía",
"noChannels": "No hay canales disponibles",
"noDvr": "No hay DVR configurado en ningún servidor",
"tuneFailed": "Error al sintonizar el canal",
"loading": "Cargando canales...",
"nowPlaying": "Reproduciendo ahora",
"noPrograms": "No hay datos de programación disponibles",
"channelNumber": "Canal ${number}",
"live": "EN VIVO",
"hd": "HD",
"premiere": "NUEVO",
"reloadGuide": "Recargar guía",
"allChannels": "Todos los canales",
"now": "Ahora",
"today": "Hoy",
"midnight": "Medianoche",
@@ -588,7 +605,13 @@
"evening": "Noche",
"lateNight": "Trasnoche",
"whatsOn": "En emisión",
"watchChannel": "Ver canal"
"watchChannel": "Ver canal",
"favorites": "Favoritos",
"reorderFavorites": "Reordenar favoritos",
"joinSession": "Unirse a sesión en curso",
"watchFromStart": "Ver desde el inicio (hace ${minutes} min)",
"watchLive": "Ver en vivo",
"goToLive": "Ir a en vivo"
},
"collections": {
"title": "Colecciones",
@@ -602,7 +625,6 @@
"deleteFailedWithError": "Error al eliminar la colección: ${error}",
"failedToLoadItems": "Error al cargar los elementos de la colección: ${error}",
"selectCollection": "Seleccionar Colección",
"createNewCollection": "Crear Nueva Colección",
"collectionName": "Nombre de la Colección",
"enterCollectionName": "Introduce el nombre de la colección",
"addedToCollection": "Añadido a la colección",
@@ -612,7 +634,8 @@
"removeFromCollectionConfirm": "¿Eliminar \"${title}\" de esta colección?",
"removedFromCollection": "Eliminado de la colección",
"removeFromCollectionFailed": "Error al eliminar de la colección",
"removeFromCollectionError": "Error al eliminar de la colección: ${error}"
"removeFromCollectionError": "Error al eliminar de la colección: ${error}",
"searchCollections": "Buscar colecciones..."
},
"playlists": {
"title": "Listas de reproducción",
@@ -634,7 +657,6 @@
"itemAdded": "Añadido a la lista",
"itemRemoved": "Eliminado de la lista",
"selectPlaylist": "Seleccionar Lista",
"createNewPlaylist": "Crear Nueva Lista",
"errorCreating": "Error al crear la lista",
"errorDeleting": "Error al eliminar la lista",
"errorLoading": "Error al cargar las listas",
@@ -677,18 +699,30 @@
"leave": "Salir",
"syncing": "Sincronizando...",
"joinWatchSession": "Unirse a Sesión de Visualización",
"enterCodeHint": "Introduce el código de 8 caracteres",
"enterCodeHint": "Introduce el código de 5 caracteres",
"pasteFromClipboard": "Pegar desde el portapapeles",
"pleaseEnterCode": "Por favor, introduce un código de sesión",
"codeMustBe8Chars": "El código de sesión debe tener 8 caracteres",
"codeMustBe5Chars": "El código de sesión debe tener 5 caracteres",
"joinInstructions": "Introduce el código de sesión compartido por el anfitrión para unirte a su sesión.",
"failedToCreate": "Error al crear la sesión",
"failedToJoin": "Error al unirse a la sesión",
"sessionCodeCopied": "Código de sesión copiado al portapapeles",
"relayUnreachable": "El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.",
"reconnectingToHost": "Reconectando con el anfitrión...",
"currentPlayback": "Reproducción actual",
"joinCurrentPlayback": "Unirse a la reproducción actual",
"joinCurrentPlaybackDescription": "Vuelve a lo que el anfitrión está viendo ahora mismo",
"failedToOpenCurrentPlayback": "No se pudo abrir la reproducción actual",
"participantJoined": "${name} se unió",
"participantLeft": "${name} se fue"
"participantLeft": "${name} se fue",
"participantPaused": "${name} pausó",
"participantResumed": "${name} reanudó",
"participantSeeked": "${name} avanzó",
"participantBuffering": "${name} está cargando",
"waitingForParticipants": "Esperando a que otros carguen...",
"recentRooms": "Salas recientes",
"renameRoom": "Renombrar sala",
"removeRoom": "Eliminar"
},
"downloads": {
"title": "Descargas",
@@ -701,6 +735,7 @@
"deleteDownload": "Eliminar descarga",
"retryDownload": "Reintentar descarga",
"downloadQueued": "Descarga en cola",
"serverErrorBitrate": "Error del servidor — el archivo puede exceder el límite de bitrate de transmisión remota",
"episodesQueued": "${count} episodios en cola para descargar",
"downloadDeleted": "Descarga eliminada",
"deleteConfirm": "¿Estás seguro de que quieres eliminar \"${title}\"? Esto borrará el archivo descargado de tu dispositivo.",
@@ -708,7 +743,32 @@
"noDownloadsTree": "Sin descargas",
"pauseAll": "Pausar todo",
"resumeAll": "Reanudar todo",
"deleteAll": "Eliminar todo"
"deleteAll": "Eliminar todo",
"selectVersion": "Seleccionar versión",
"allEpisodes": "Todos los episodios",
"unwatchedOnly": "Solo no vistos",
"nextNUnwatched": "Próximos ${count} no vistos",
"customAmount": "Cantidad personalizada...",
"howManyEpisodes": "¿Cuántos episodios?",
"itemsQueued": "${count} elementos en cola de descarga",
"keepSynced": "Mantener sincronizado",
"downloadOnce": "Descargar una vez",
"keepNUnwatched": "Mantener ${count} sin ver",
"editSyncRule": "Editar regla de sincronización",
"removeSyncRule": "Eliminar regla de sincronización",
"removeSyncRuleConfirm": "¿Dejar de sincronizar \"${title}\"? Los episodios descargados se conservarán.",
"syncRuleCreated": "Regla de sincronización creada — conservando ${count} episodios sin ver",
"syncRuleUpdated": "Regla de sincronización actualizada",
"syncRuleRemoved": "Regla de sincronización eliminada",
"syncedNewEpisodes": "${count} nuevos episodios sincronizados para ${title}",
"activeSyncRules": "Reglas de sincronización",
"noSyncRules": "Sin reglas de sincronización",
"manageSyncRule": "Gestionar sincronización",
"editEpisodeCount": "Número de episodios",
"editSyncFilter": "Filtro de sincronización",
"syncAllItems": "Sincronizando todos los elementos",
"syncUnwatchedItems": "Sincronizando elementos no vistos",
"syncRuleListCreated": "Regla de sincronización creada"
},
"shaders": {
"title": "Shaders",
@@ -716,58 +776,46 @@
"nvscalerDescription": "Escalado de imagen NVIDIA para un video más nítido",
"qualityFast": "Rápido",
"qualityHQ": "Alta Calidad",
"mode": "Modo"
"mode": "Modo",
"importShader": "Importar shader",
"customShaderDescription": "Shader GLSL personalizado",
"shaderImported": "Shader importado",
"shaderImportFailed": "Error al importar shader",
"deleteShader": "Eliminar shader",
"deleteShaderConfirm": "¿Eliminar \"${name}\"?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Conectar a dispositivo",
"hostRemoteSession": "Iniciar sesión remota",
"controlThisDevice": "Controla este dispositivo con tu teléfono",
"remoteControl": "Control remoto",
"controlDesktop": "Controlar un dispositivo de escritorio",
"title": "Control remoto",
"connectedTo": "Conectado a ${name}",
"session": {
"creatingSession": "Creando sesión remota...",
"failedToCreate": "Error al crear la sesión remota:",
"noSession": "No hay sesión disponible",
"scanQrCode": "Escanear código QR",
"orEnterManually": "O introducir manualmente",
"startingServer": "Iniciando servidor remoto...",
"failedToCreate": "Error al iniciar el servidor remoto:",
"hostAddress": "Dirección del host",
"sessionId": "ID de sesión",
"pin": "PIN",
"connected": "Conectado",
"waitingForConnection": "Esperando conexión...",
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta app",
"copiedToClipboard": "${label} copiado al portapapeles",
"copyToClipboard": "Copiar al portapapeles",
"newSession": "Nueva sesión",
"serverRunning": "Servidor remoto activo",
"serverStopped": "Servidor remoto detenido",
"serverRunningDescription": "Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación",
"serverStoppedDescription": "Inicia el servidor para permitir que los dispositivos móviles se conecten",
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta aplicación",
"startServer": "Iniciar servidor",
"stopServer": "Detener servidor",
"minimize": "Minimizar"
},
"pairing": {
"scan": "Escanear",
"manual": "Manual",
"pairWithDesktop": "Emparejar con escritorio",
"enterSessionDetails": "Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio",
"discoveryDescription": "Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Introduce ID de sesión de 8 caracteres",
"pinHint": "Introduce PIN de 6 dígitos",
"connecting": "Conectando...",
"tips": "Consejos",
"tipDesktop": "Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú",
"tipScan": "Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio",
"tipWifi": "Asegúrate de que ambos dispositivos estén en la misma red WiFi",
"cameraPermissionRequired": "Se necesita permiso de cámara para escanear códigos QR.\nPor favor, concede acceso a la cámara en los ajustes de tu dispositivo.",
"cameraError": "No se pudo iniciar la cámara: ${error}",
"scanInstruction": "Apunta tu cámara al código QR que aparece en tu escritorio",
"invalidQrCode": "Formato de código QR no válido",
"validationHostRequired": "Por favor, introduce la dirección del host",
"validationHostFormat": "El formato debe ser IP:puerto (ej., 192.168.1.100:48632)",
"validationSessionIdRequired": "Por favor, introduce un ID de sesión",
"validationSessionIdLength": "El ID de sesión debe tener 8 caracteres",
"validationPinRequired": "Por favor, introduce un PIN",
"validationPinLength": "El PIN debe tener 6 dígitos",
"connectionTimedOut": "Tiempo de conexión agotado. Verifica el ID de sesión y el PIN.",
"sessionNotFound": "No se encontró la sesión. Verifica tus credenciales.",
"searchingForDevices": "Buscando dispositivos...",
"noDevicesFound": "No se encontraron dispositivos en tu red",
"noDevicesHint": "Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi",
"availableDevices": "Dispositivos disponibles",
"manualConnection": "Conexión manual",
"cryptoInitFailed": "No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.",
"validationHostRequired": "Ingresa la dirección del host",
"validationHostFormat": "El formato debe ser IP:puerto (ej. 192.168.1.100:48632)",
"connectionTimedOut": "Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.",
"sessionNotFound": "No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.",
"authFailed": "Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.",
"failedToConnect": "Error al conectar: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Reconectando...",
"attemptOf": "Intento ${current} de 5",
"retryNow": "Reintentar ahora",
"connectionError": "Error de conexión",
"notConnected": "No conectado",
"tabRemote": "Remoto",
"tabPlay": "Reproducir",
"tabMore": "Más",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Usar reproductor externo",
"useExternalPlayerDescription": "Abrir vídeos en una app externa en lugar del reproductor integrado",
"selectPlayer": "Seleccionar reproductor",
"customPlayers": "Reproductores personalizados",
"systemDefault": "Predeterminado del sistema",
"addCustomPlayer": "Añadir reproductor personalizado",
"playerName": "Nombre del reproductor",
"playerCommand": "Comando",
"playerPackage": "Nombre del paquete",
"playerUrlScheme": "Esquema URL",
"customPlayer": "Reproductor personalizado",
"off": "Desactivado",
"launchFailed": "No se pudo abrir el reproductor externo",
"appNotInstalled": "${name} no está instalado",
@@ -847,8 +893,12 @@
"summary": "Resumen",
"poster": "Póster",
"background": "Fondo",
"logo": "Logo",
"squareArt": "Imagen cuadrada",
"selectPoster": "Seleccionar póster",
"selectBackground": "Seleccionar fondo",
"selectLogo": "Seleccionar logo",
"selectSquareArt": "Seleccionar imagen cuadrada",
"fromUrl": "Desde URL",
"uploadFile": "Subir archivo",
"enterImageUrl": "Introducir URL de imagen",
@@ -890,6 +940,39 @@
"subtitleMode": "Selección automática de subtítulos",
"manuallySelected": "Seleccionado manualmente",
"shownWithForeignAudio": "Mostrar con audio extranjero",
"alwaysEnabled": "Siempre activado"
"alwaysEnabled": "Siempre activado",
"tags": "Etiquetas",
"addTag": "Añadir etiqueta",
"genre": "Género",
"director": "Director",
"writer": "Guionista",
"producer": "Productor",
"country": "País",
"collection": "Colección",
"label": "Etiqueta",
"style": "Estilo",
"mood": "Estado de ánimo"
},
"serverTasks": {
"title": "Tareas del servidor",
"failedToLoad": "Error al cargar tareas",
"noTasks": "No hay tareas en ejecución"
},
"trakt": {
"title": "Trakt",
"connected": "Conectado",
"connectedAs": "Conectado como @${username}",
"disconnectConfirm": "¿Desconectar cuenta de Trakt?",
"disconnectConfirmBody": "Plezy dejará de enviar eventos de reproducción a Trakt. Puedes volver a conectar cuando quieras.",
"scrobble": "Scrobbling en tiempo real",
"scrobbleDescription": "Enviar eventos de reproducción, pausa y parada a Trakt durante la reproducción.",
"watchedSync": "Sincronizar estado de visto",
"watchedSyncDescription": "Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt.",
"deviceCodeTitle": "Activa Plezy en Trakt",
"deviceCodeBody": "Visita ${url} e introduce este código:",
"openTraktActivate": "Abrir Trakt para activar",
"waitingForAuthorization": "Esperando autorización…",
"codeCopied": "Código copiado",
"connectFailed": "No se pudo conectar a Trakt. Inténtalo de nuevo."
}
}
+216 -133
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "S'inscrire avec Plex",
"showQRCode": "Afficher le QR Code",
"authenticate": "S'authentifier",
"debugEnterToken": "Debug: Entrez votre token Plex",
"plexTokenLabel": "Token d'authentification Plex",
"plexTokenHint": "Entrez votre token Plex.tv",
"authenticationTimeout": "Délai d'authentification expiré. Veuillez réessayer.",
"scanQRToSignIn": "Scannez ce QR code pour vous connecter",
"waitingForAuth": "En attente d'authentification...\nVeuillez vous connecter dans votre navigateur.",
@@ -32,8 +29,7 @@
"delete": "Supprimer",
"shuffle": "Mélanger",
"addTo": "Ajouter à...",
"remove": "Supprimer",
"paste": "Coller",
"createNew": "Créer",
"connect": "Connecter",
"disconnect": "Déconnecter",
"play": "Lire",
@@ -43,10 +39,9 @@
"search": "Recherche",
"home": "Accueil",
"back": "Retour",
"settings": "Paramètres",
"settings": "Réglages",
"mute": "Muet",
"ok": "OK",
"loading": "Chargement...",
"reconnect": "Reconnecter",
"exitConfirmTitle": "Quitter l'application ?",
"exitConfirmMessage": "Êtes-vous sûr de vouloir quitter ?",
@@ -57,13 +52,14 @@
"refreshingServers": "Actualisation des serveurs...",
"loadingServers": "Chargement des serveurs...",
"connectingToServers": "Connexion aux serveurs...",
"startingOfflineMode": "Démarrage en mode hors-ligne..."
"startingOfflineMode": "Démarrage en mode hors-ligne...",
"loading": "Chargement..."
},
"screens": {
"licenses": "Licenses",
"switchProfile": "Changer de profil",
"subtitleStyling": "Configuration des sous-titres",
"mpvConfig": "Configuration MPV",
"mpvConfig": "mpv.conf",
"logs": "Logs"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Paramètres",
"supportDeveloper": "Soutenir Plezy",
"supportDeveloperDescription": "Faites un don via Liberapay pour financer le développement",
"language": "Langue",
"theme": "Thème",
"appearance": "Apparence",
"videoPlayback": "Lecture vidéo",
"videoPlaybackDescription": "Configurer le comportement de lecture",
"advanced": "Avancé",
"episodePosterMode": "Style du Poster d'épisode",
"seriesPoster": "Poster de série",
"seriesPosterDescription": "Afficher le poster de série pour tous les épisodes",
"seasonPoster": "Poster de saison",
"seasonPosterDescription": "Afficher le poster spécifique à la saison pour les épisodes",
"episodeThumbnail": "Mignature d'épisode",
"episodeThumbnailDescription": "Afficher les vignettes des captures d'écran des épisodes au format 16:9",
"episodeThumbnail": "Miniature",
"showHeroSectionDescription": "Afficher le carrousel de contenu en vedette sur l'écran d'accueil",
"secondsLabel": "Secondes",
"minutesLabel": "Minutes",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Entrez la durée (${min}-${max})",
"systemTheme": "Système",
"systemThemeDescription": "Suivre les paramètres système",
"lightTheme": "Light",
"darkTheme": "Dark",
"lightTheme": "Clair",
"darkTheme": "Sombre",
"oledTheme": "OLED",
"oledThemeDescription": "Noir pur pour les écrans OLED",
"libraryDensity": "Densité des bibliothèques",
"compact": "Compact",
"compactDescription": "Cartes plus petites, plus d'éléments visibles",
"normal": "Normal",
"normalDescription": "Taille par défaut",
"comfortable": "Confortable",
"comfortableDescription": "Cartes plus grandes, moins d'éléments visibles",
"viewMode": "Mode d'affichage",
"gridView": "Grille",
"gridViewDescription": "Afficher les éléments dans une disposition en grille",
"listView": "Liste",
"listViewDescription": "Afficher les éléments dans une liste",
"showHeroSection": "Afficher la section Hero",
"useGlobalHubs": "Utiliser la disposition Plex Home",
"useGlobalHubsDescription": "Afficher les hubs de la page d'accueil comme le client Plex officiel. Lorsque cette option est désactivée, affiche à la place les recommandations par bibliothèque.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Flouter les miniatures et masquer les descriptions des épisodes que vous n'avez pas encore regardés",
"playerBackend": "Moteur de lecture",
"exoPlayer": "ExoPlayer (Recommandé)",
"exoPlayerDescription": "Lecteur natif Android avec meilleur support matériel",
"mpv": "MPV",
"mpvDescription": "Lecteur avancé avec plus de fonctionnalités et support des sous-titres ASS",
"mpv": "mpv",
"hardwareDecoding": "Décodage matériel",
"hardwareDecodingDescription": "Utilisez l'accélération matérielle lorsqu'elle est disponible.",
"bufferSize": "Taille du Buffer",
@@ -137,8 +123,9 @@
"bufferSizeWarning": "Votre appareil dispose de ${heap}MB de mémoire. Un tampon de ${size}MB peut causer des problèmes de lecture.",
"subtitleStyling": "Stylisation des sous-titres",
"subtitleStylingDescription": "Personnaliser l'apparence des sous-titres",
"smallSkipDuration": "Small Skip Duration",
"largeSkipDuration": "Large Skip Duration",
"smallSkipDuration": "Durée du petit saut",
"largeSkipDuration": "Durée du grand saut",
"rewindOnResume": "Rembobiner à la reprise",
"secondsUnit": "${seconds} secondes",
"defaultSleepTimer": "Minuterie de mise en veille par défaut",
"minutesUnit": "${minutes} minutes",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Personnaliser les raccourcis clavier",
"videoPlayerNavigation": "Navigation dans le lecteur vidéo",
"videoPlayerNavigationDescription": "Utilisez les touches fléchées pour naviguer dans les commandes du lecteur vidéo.",
"watchTogetherRelay": "Relais Regarder Ensemble",
"watchTogetherRelayDescription": "Définir un serveur relais personnalisé pour Regarder Ensemble. Tous les participants doivent utiliser le même serveur.",
"watchTogetherRelayHint": "https://mon-relais.exemple.fr",
"crashReporting": "Rapports de plantage",
"crashReportingDescription": "Envoyer des rapports de plantage pour améliorer l'application",
"debugLogging": "Journalisation de débogage",
"debugLoggingDescription": "Activer la journalisation détaillée pour le dépannage",
"viewLogs": "Voir les logs",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Passer les crédits et passer à l'épisode suivant automatiquement",
"autoSkipDelay": "Délai avant skip automatique",
"autoSkipDelayDescription": "Attendre ${seconds} secondes avant l'auto-skip",
"introPattern": "Modèle de marqueur d'intro",
"introPatternDescription": "Expression régulière pour reconnaître les marqueurs d'intro dans les titres de chapitres",
"creditsPattern": "Modèle de marqueur de générique",
"creditsPatternDescription": "Expression régulière pour reconnaître les marqueurs de générique dans les titres de chapitres",
"invalidRegex": "Expression régulière invalide",
"downloads": "Téléchargement",
"downloadLocationDescription": "Choisissez où stocker le contenu téléchargé",
"downloadLocationDefault": "Par défaut (stockage de l'application)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Échec de la sélection du dossier",
"downloadOnWifiOnly": "Télécharger uniquement via WiFi",
"downloadOnWifiOnlyDescription": "Empêcher les téléchargements lorsque vous utilisez les données cellulaires",
"autoRemoveWatchedDownloads": "Supprimer automatiquement les téléchargements vus",
"autoRemoveWatchedDownloadsDescription": "Supprimer automatiquement les épisodes et films téléchargés lorsqu'ils sont marqués comme vus",
"cellularDownloadBlocked": "Les téléchargements sont désactivés sur les données cellulaires. Connectez-vous au Wi-Fi ou modifiez le paramètre.",
"maxVolume": "Volume maximal",
"maxVolumeDescription": "Autoriser l'augmentation du volume au-delà de 100 % pour les médias silencieux",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Montrez ce que vous regardez sur Discord",
"trakt": "Trakt",
"traktDescription": "Synchroniser l'historique de visionnage avec Trakt",
"companionRemoteServer": "Serveur de télécommande",
"companionRemoteServerDescription": "Autoriser les appareils mobiles de votre réseau à contrôler cette application",
"autoPip": "Image dans l'image automatique",
"autoPipDescription": "Activer automatiquement l'image dans l'image en quittant l'application pendant la lecture",
"matchContentFrameRate": "Fréquence d'images du contenu correspondant",
"matchContentFrameRateDescription": "Ajustez la fréquence de rafraîchissement de l'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Adapter la fréquence de rafraîchissement",
"matchRefreshRateDescription": "Changer la fréquence de rafraîchissement de l'écran pour correspondre au contenu vidéo en plein écran",
"matchDynamicRange": "Adapter la plage dynamique",
"matchDynamicRangeDescription": "Activer automatiquement le HDR pour le contenu HDR et revenir en SDR en quittant le lecteur",
"displaySwitchDelay": "Délai de changement d'affichage",
"tunneledPlayback": "Lecture tunnelée",
"tunneledPlaybackDescription": "Utiliser le tunnelage vidéo accéléré par matériel. Désactiver si vous voyez un écran noir avec du son sur du contenu HDR",
"requireProfileSelectionOnOpen": "Demander le profil à l'ouverture",
"requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application",
"confirmExitOnBack": "Confirmer avant de quitter",
"confirmExitOnBackDescription": "Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter"
"confirmExitOnBackDescription": "Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter",
"forceTvMode": "Forcer le mode TV",
"forceTvModeDescription": "Utiliser la mise en page TV quelle que soit la détection automatique. Utile sur les appareils Android TV qui ne signalent pas la fonctionnalité leanback. Redémarre l'application lors du changement.",
"autoHidePerformanceOverlay": "Masquer auto. superposition performances",
"autoHidePerformanceOverlayDescription": "Faire apparaître/disparaître la superposition avec les contrôles de lecture",
"showNavBarLabels": "Afficher les libellés de la barre de navigation",
"showNavBarLabelsDescription": "Afficher les libellés sous les icônes de la barre de navigation",
"liveTvDefaultFavorites": "Chaînes favorites par défaut",
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Rechercher des films, des séries, de la musique...",
@@ -239,11 +263,6 @@
"skipMarker": "Passer l'intro/le générique"
}
},
"pinEntry": {
"enterPin": "Entrer le code PIN",
"showPin": "Afficher le code PIN",
"hidePin": "Masquer le code PIN"
},
"fileInfo": {
"title": "Informations sur le fichier",
"video": "Vidéo",
@@ -261,7 +280,9 @@
"colorRange": "Gamme de couleurs",
"colorPrimaries": "Couleurs primaires",
"chromaSubsampling": "Sous-échantillonnage chromatique",
"channels": "Channels",
"channels": "Canaux",
"subtitles": "Sous-titres",
"overallBitrate": "Débit global",
"path": "Chemin",
"size": "Taille",
"container": "Conteneur",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Cela inclut tous les épisodes et leurs fichiers.",
"mediaDeletedSuccessfully": "Élément média supprimé avec succès",
"mediaFailedToDelete": "Échec de la suppression de l'élément média",
"rate": "Noter"
"rate": "Noter",
"playFromBeginning": "Lire depuis le début",
"playVersion": "Lire la version..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
@@ -316,7 +339,6 @@
"unlockRotation": "Déverrouiller la rotation",
"timerActive": "Minuterie active",
"playbackWillPauseIn": "La lecture sera mise en pause dans ${duration}",
"sleepTimerCompleted": "Minuterie de mise en veille terminée - lecture en pause",
"stillWatching": "Toujours en train de regarder ?",
"pausingIn": "Pause dans ${seconds}s",
"continueWatching": "Continuer",
@@ -333,34 +355,43 @@
"muteButton": "Mute",
"unmuteButton": "Dé-mute",
"settingsButton": "Paramètres vidéo",
"audioTrackButton": "Pistes audio",
"subtitlesButton": "Sous-titres",
"tracksButton": "Audio et sous-titres",
"chaptersButton": "Chapitres",
"versionsButton": "Versions vidéo",
"pipButton": "Mode PiP (Picture-in-Picture)",
"aspectRatioButton": "Format d'image",
"ambientLighting": "Éclairage ambiant",
"ambientLightingOn": "Activer l'éclairage ambiant",
"ambientLightingOff": "Désactiver l'éclairage ambiant",
"fullscreenButton": "Passer en mode plein écran",
"exitFullscreenButton": "Quitter le mode plein écran",
"alwaysOnTopButton": "Always on top",
"alwaysOnTopButton": "Toujours au premier plan",
"rotationLockButton": "Verrouillage de rotation",
"lockScreen": "Verrouiller l'écran",
"screenLockButton": "Verrouillage de l'écran",
"longPressToUnlock": "Appui long pour déverrouiller",
"timelineSlider": "Timeline vidéo",
"volumeSlider": "Niveau sonore",
"endsAt": "Fin à ${time}",
"pipActive": "Lecture en mode image dans l'image",
"pipFailed": "Échec du démarrage du mode image dans l'image",
"pipErrors": {
"androidVersion": "Nécessite Android 8.0 ou plus récent",
"iosVersion": "Nécessite iOS 15.0 ou plus récent",
"permissionDisabled": "L'autorisation Image dans l'image est désactivée. Activez-la dans Paramètres > Applications > Plezy > Image dans l'image",
"notSupported": "Cet appareil ne prend pas en charge le mode image dans l'image",
"voSwitchFailed": "Échec du changement de sortie vidéo pour l'image dans l'image",
"failed": "Échec du démarrage du mode image dans l'image",
"unknown": "Une erreur s'est produite : ${error}"
},
"chapters": "Chapitres",
"noChaptersAvailable": "Aucun chapitre disponible",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "File d'attente",
"noQueueItems": "Aucun élément dans la file d'attente",
"searchSubtitles": "Rechercher des sous-titres",
"language": "Langue",
"noSubtitlesFound": "Aucun sous-titre trouvé",
"subtitleDownloaded": "Sous-titre téléchargé",
"subtitleDownloadFailed": "Échec du téléchargement du sous-titre",
"searchLanguages": "Rechercher des langues..."
},
"userStatus": {
"admin": "Admin",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Marqué comme non vu",
"markedAsWatchedOffline": "Marqué comme vu (se synchronisera lorsque vous serez en ligne)",
"markedAsUnwatchedOffline": "Marqué comme non vu (sera synchronisé lorsque vous serez en ligne)",
"autoRemovedWatchedDownload": "Supprimé automatiquement : ${title}",
"removedFromContinueWatching": "Supprimer de \"Continuer à regarder\"",
"errorLoading": "Erreur: ${error}",
"fileInfoNotAvailable": "Informations sur le fichier non disponibles",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Échec de la création de la file d'attente de lecture - aucun élément",
"failedPlayback": "Echec de ${action}: ${error}",
"switchingToCompatiblePlayer": "Passage au lecteur compatible...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Échec de la lecture",
"serverLimitBody": "Le serveur a renvoyé une erreur (HTTP 500). Cela signifie généralement que le propriétaire du serveur a défini une limite de bande passante ou de transcodage qui rejette ta session. Rien à faire côté client — le propriétaire du serveur doit ajuster ses paramètres.",
"logsUploaded": "Logs envoyés",
"logsUploadFailed": "Échec de l'envoi des logs",
"logId": "ID du log"
},
"subtitlingStyling": {
"stylingOptions": "Options de style",
"text": "Texte",
"border": "Bordure",
"background": "Arrière-plan",
"fontSize": "Taille de la police",
"textColor": "Couleur du texte",
"borderSize": "Taille de la bordure",
"borderColor": "Couleur de la bordure",
"backgroundOpacity": "Opacité d'arrière-plan",
"backgroundColor": "Couleur d'arrière-plan",
"position": "Position"
"position": "Position",
"assOverride": "Remplacement ASS",
"bold": "Gras",
"italic": "Italique"
},
"mpvConfig": {
"title": "Configuration MPV",
"title": "Configuration mpv",
"description": "Paramètres avancés du lecteur vidéo",
"properties": "Propriétés",
"presets": "Préréglages",
"noProperties": "Aucune propriété configurée",
"noPresets": "Aucun préréglage enregistré",
"addProperty": "Ajouter une propriété",
"editProperty": "Modifier la propriété",
"deleteProperty": "Supprimer la propriété",
"propertyKey": "Clé",
"propertyKeyHint": "e.g., hwdec, demuxer-max-bytes",
"propertyValue": "Valeur",
"propertyValueHint": "e.g., auto, 256000000",
"saveAsPreset": "Enregistrer comme préréglage...",
"presetName": "Nom du préréglage",
"presetNameHint": "Entrez un nom pour ce préréglage",
@@ -436,8 +466,7 @@
"presetLoaded": "Préréglage chargé",
"presetDeleted": "Préréglage supprimé",
"confirmDeletePreset": "Êtes-vous sûr de vouloir supprimer ce préréglage ?",
"confirmDeleteProperty": "Êtes-vous sûr de vouloir supprimer cette propriété ?",
"entriesCount": "${count} entrées"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Confirmer l'action"
@@ -452,11 +481,8 @@
"overview": "Aperçu",
"cast": "Cast",
"extras": "Bandes-annonces et Extras",
"seasons": "Saisons",
"studio": "Studio",
"rating": "Évaluation",
"episodeCount": "${count} épisodes",
"watchedProgress": "${watched}/${total} vu",
"movie": "Film",
"tvShow": "Show TV",
"minutesLeft": "${minutes} min restantes"
@@ -518,6 +544,7 @@
"playlists": "Playlists"
},
"groupings": {
"title": "Regroupement",
"all": "Tous",
"movies": "Films",
"shows": "Show TV",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Effacer les logs",
"copyLogs": "Copier les logs",
"uploadLogs": "Upload Logs",
"error": "Erreur:",
"stackTrace": "Liste des appels:"
"uploadLogs": "Envoyer les logs"
},
"licenses": {
"relatedPackages": "Package associés",
@@ -559,26 +584,18 @@
"licensesCount": "${count} licences"
},
"navigation": {
"libraries": "Bibliothèques",
"downloads": "Téléchargements",
"liveTv": "TV en direct"
"libraries": "Medias",
"downloads": "Téléch.",
"liveTv": "TV direct"
},
"liveTv": {
"title": "TV en direct",
"channels": "Chaînes",
"guide": "Guide",
"noChannels": "Aucune chaîne disponible",
"noDvr": "Aucun DVR configuré sur les serveurs",
"tuneFailed": "Impossible de syntoniser la chaîne",
"loading": "Chargement des chaînes...",
"nowPlaying": "En cours de lecture",
"noPrograms": "Aucune donnée de programme disponible",
"channelNumber": "Ch. ${number}",
"live": "EN DIRECT",
"hd": "HD",
"premiere": "NOUVEAU",
"reloadGuide": "Recharger le guide",
"allChannels": "Toutes les chaînes",
"now": "Maintenant",
"today": "Aujourd'hui",
"midnight": "Minuit",
@@ -588,7 +605,13 @@
"evening": "Soirée",
"lateNight": "Nuit tardive",
"whatsOn": "En ce moment",
"watchChannel": "Regarder la chaîne"
"watchChannel": "Regarder la chaîne",
"favorites": "Favoris",
"reorderFavorites": "Réorganiser les favoris",
"joinSession": "Rejoindre la session en cours",
"watchFromStart": "Regarder depuis le début (il y a ${minutes} min)",
"watchLive": "Regarder en direct",
"goToLive": "Aller au direct"
},
"collections": {
"title": "Collections",
@@ -602,7 +625,6 @@
"deleteFailedWithError": "Échec de la suppression de la collection: ${error}",
"failedToLoadItems": "Échec du chargement des éléments de la collection: ${error}",
"selectCollection": "Sélectionner une collection",
"createNewCollection": "Créer une nouvelle collection",
"collectionName": "Nom de la collection",
"enterCollectionName": "Entrez le nom de la collection",
"addedToCollection": "Ajouté à la collection",
@@ -612,7 +634,8 @@
"removeFromCollectionConfirm": "Retirer \"${title}\" de cette collection ?",
"removedFromCollection": "Retiré de la collection",
"removeFromCollectionFailed": "Impossible de supprimer de la collection",
"removeFromCollectionError": "Erreur lors de la suppression de la collection: ${error}"
"removeFromCollectionError": "Erreur lors de la suppression de la collection: ${error}",
"searchCollections": "Rechercher des collections..."
},
"playlists": {
"title": "Playlists",
@@ -633,8 +656,7 @@
"deleted": "Playlist supprimée",
"itemAdded": "Ajouté à la playlist",
"itemRemoved": "Retiré de la playlist",
"selectPlaylist": "Select Playlist",
"createNewPlaylist": "Créer une nouvelle playlist",
"selectPlaylist": "Sélectionner une playlist",
"errorCreating": "Échec de la création de playlist",
"errorDeleting": "Échec de suppression de playlist",
"errorLoading": "Échec de chargement de playlists",
@@ -677,18 +699,30 @@
"leave": "Fin",
"syncing": "Synchronisation...",
"joinWatchSession": "Rejoindre la session de visionnage",
"enterCodeHint": "Entrez le code à 8 caractères",
"enterCodeHint": "Entrez le code à 5 caractères",
"pasteFromClipboard": "Coller depuis le presse-papiers",
"pleaseEnterCode": "Veuillez saisir un code de session",
"codeMustBe8Chars": "Le code de session doit comporter 8 caractères",
"codeMustBe5Chars": "Le code de session doit comporter 5 caractères",
"joinInstructions": "Entrez le code de session partagé par l'hôte pour rejoindre sa session de visionnage.",
"failedToCreate": "Échec de la création de la session",
"failedToJoin": "Échec de la connexion à la session",
"sessionCodeCopied": "Code de session copié dans le presse-papiers",
"relayUnreachable": "Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.",
"reconnectingToHost": "Reconnexion à l'hôte...",
"currentPlayback": "Lecture en cours",
"joinCurrentPlayback": "Rejoindre la lecture en cours",
"joinCurrentPlaybackDescription": "Revenez à ce que l'hôte regarde actuellement",
"failedToOpenCurrentPlayback": "Impossible d'ouvrir la lecture en cours",
"participantJoined": "${name} a rejoint",
"participantLeft": "${name} est parti"
"participantLeft": "${name} est parti",
"participantPaused": "${name} a mis en pause",
"participantResumed": "${name} a repris",
"participantSeeked": "${name} a avancé",
"participantBuffering": "${name} met en mémoire tampon",
"waitingForParticipants": "En attente du chargement des autres...",
"recentRooms": "Salons récents",
"renameRoom": "Renommer le salon",
"removeRoom": "Supprimer"
},
"downloads": {
"title": "Téléchargements",
@@ -701,6 +735,7 @@
"deleteDownload": "Supprimer le téléchargement",
"retryDownload": "Réessayer le téléchargement",
"downloadQueued": "Téléchargement en attente",
"serverErrorBitrate": "Erreur serveur — le fichier dépasse peut-être la limite de débit du streaming à distance",
"episodesQueued": "${count} épisodes en attente de téléchargement",
"downloadDeleted": "Télécharger supprimé",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer \"${title}\" ? Cela supprimera le fichier téléchargé de votre appareil.",
@@ -708,7 +743,32 @@
"noDownloadsTree": "Aucun téléchargement",
"pauseAll": "Tout mettre en pause",
"resumeAll": "Tout reprendre",
"deleteAll": "Tout supprimer"
"deleteAll": "Tout supprimer",
"selectVersion": "Sélectionner la version",
"allEpisodes": "Tous les épisodes",
"unwatchedOnly": "Non vus uniquement",
"nextNUnwatched": "${count} prochains non vus",
"customAmount": "Quantité personnalisée...",
"howManyEpisodes": "Combien d'épisodes ?",
"itemsQueued": "${count} éléments mis en file d'attente",
"keepSynced": "Garder synchronisé",
"downloadOnce": "Télécharger une fois",
"keepNUnwatched": "Garder ${count} non vus",
"editSyncRule": "Modifier la règle de synchronisation",
"removeSyncRule": "Supprimer la règle de synchronisation",
"removeSyncRuleConfirm": "Arrêter la synchronisation de « ${title} » ? Les épisodes téléchargés seront conservés.",
"syncRuleCreated": "Règle de synchronisation créée — ${count} épisodes non vus conservés",
"syncRuleUpdated": "Règle de synchronisation mise à jour",
"syncRuleRemoved": "Règle de synchronisation supprimée",
"syncedNewEpisodes": "${count} nouveaux épisodes synchronisés pour ${title}",
"activeSyncRules": "Règles de synchronisation",
"noSyncRules": "Aucune règle de synchronisation",
"manageSyncRule": "Gérer la synchronisation",
"editEpisodeCount": "Nombre d’épisodes",
"editSyncFilter": "Filtre de synchronisation",
"syncAllItems": "Synchronisation de tous les éléments",
"syncUnwatchedItems": "Synchronisation des éléments non vus",
"syncRuleListCreated": "Règle de synchronisation créée"
},
"shaders": {
"title": "Shaders",
@@ -716,58 +776,46 @@
"nvscalerDescription": "Mise à l'échelle NVIDIA pour une vidéo plus nette",
"qualityFast": "Rapide",
"qualityHQ": "Haute qualité",
"mode": "Mode"
"mode": "Mode",
"importShader": "Importer un shader",
"customShaderDescription": "Shader GLSL personnalisé",
"shaderImported": "Shader importé",
"shaderImportFailed": "Échec de l'importation du shader",
"deleteShader": "Supprimer le shader",
"deleteShaderConfirm": "Supprimer \"${name}\" ?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Se connecter à un appareil",
"hostRemoteSession": "Héberger une session distante",
"controlThisDevice": "Contrôlez cet appareil avec votre téléphone",
"remoteControl": "Télécommande",
"controlDesktop": "Contrôler un appareil de bureau",
"title": "Télécommande compagnon",
"connectedTo": "Connecté à ${name}",
"session": {
"creatingSession": "Création de la session distante...",
"failedToCreate": "Échec de la création de la session distante :",
"noSession": "Aucune session disponible",
"scanQrCode": "Scanner le QR Code",
"orEnterManually": "Ou saisir manuellement",
"startingServer": "Démarrage du serveur distant...",
"failedToCreate": "Échec du démarrage du serveur distant :",
"hostAddress": "Adresse de l'hôte",
"sessionId": "ID de session",
"pin": "PIN",
"connected": "Connecté",
"waitingForConnection": "En attente de connexion...",
"serverRunning": "Serveur distant actif",
"serverStopped": "Serveur distant arrêté",
"serverRunningDescription": "Les appareils mobiles de votre réseau peuvent découvrir et se connecter à cette application",
"serverStoppedDescription": "Démarrez le serveur pour permettre aux appareils mobiles de se connecter",
"usePhoneToControl": "Utilisez votre appareil mobile pour contrôler cette application",
"copiedToClipboard": "${label} copié dans le presse-papiers",
"copyToClipboard": "Copier dans le presse-papiers",
"newSession": "Nouvelle session",
"startServer": "Démarrer le serveur",
"stopServer": "Arrêter le serveur",
"minimize": "Réduire"
},
"pairing": {
"scan": "Scanner",
"manual": "Manuel",
"pairWithDesktop": "Jumeler avec un bureau",
"enterSessionDetails": "Saisissez les détails de la session affichés sur votre appareil de bureau",
"discoveryDescription": "Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Saisissez l'ID de session à 8 caractères",
"pinHint": "Saisissez le PIN à 6 chiffres",
"connecting": "Connexion...",
"tips": "Conseils",
"tipDesktop": "Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu",
"tipScan": "Utilisez l'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau",
"tipWifi": "Assurez-vous que les deux appareils sont sur le même réseau WiFi",
"cameraPermissionRequired": "L'autorisation de la caméra est requise pour scanner les QR codes.\nVeuillez accorder l'accès à la caméra dans les paramètres de votre appareil.",
"cameraError": "Impossible de démarrer la caméra : ${error}",
"scanInstruction": "Pointez votre caméra vers le QR code affiché sur votre bureau",
"invalidQrCode": "Format de QR code invalide",
"validationHostRequired": "Veuillez saisir l'adresse de l'hôte",
"validationHostFormat": "Le format doit être IP:port (ex : 192.168.1.100:48632)",
"validationSessionIdRequired": "Veuillez saisir un ID de session",
"validationSessionIdLength": "L'ID de session doit contenir 8 caractères",
"validationPinRequired": "Veuillez saisir un PIN",
"validationPinLength": "Le PIN doit contenir 6 chiffres",
"connectionTimedOut": "Délai de connexion expiré. Veuillez vérifier l'ID de session et le PIN.",
"sessionNotFound": "Session introuvable. Veuillez vérifier vos identifiants.",
"searchingForDevices": "Recherche d'appareils...",
"noDevicesFound": "Aucun appareil trouvé sur votre réseau",
"noDevicesHint": "Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi",
"availableDevices": "Appareils disponibles",
"manualConnection": "Connexion manuelle",
"cryptoInitFailed": "Impossible d'initialiser la connexion sécurisée. Assurez-vous d'être connecté à un compte Plex.",
"validationHostRequired": "Veuillez entrer l'adresse de l'hôte",
"validationHostFormat": "Le format doit être IP:port (ex. 192.168.1.100:48632)",
"connectionTimedOut": "Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.",
"sessionNotFound": "Appareil introuvable. Assurez-vous que Plezy est en cours d'exécution sur l'hôte.",
"authFailed": "Échec de l'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.",
"failedToConnect": "Échec de la connexion : ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Reconnexion...",
"attemptOf": "Tentative ${current} sur 5",
"retryNow": "Réessayer maintenant",
"connectionError": "Erreur de connexion",
"notConnected": "Non connecté",
"tabRemote": "Télécommande",
"tabPlay": "Lecture",
"tabMore": "Plus",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Utiliser un lecteur externe",
"useExternalPlayerDescription": "Ouvrir les vidéos dans une application externe au lieu du lecteur intégré",
"selectPlayer": "Sélectionner le lecteur",
"customPlayers": "Lecteurs personnalisés",
"systemDefault": "Par défaut du système",
"addCustomPlayer": "Ajouter un lecteur personnalisé",
"playerName": "Nom du lecteur",
"playerCommand": "Commande",
"playerPackage": "Nom du paquet",
"playerUrlScheme": "Schéma URL",
"customPlayer": "Lecteur personnalisé",
"off": "Désactivé",
"launchFailed": "Impossible d'ouvrir le lecteur externe",
"appNotInstalled": "${name} n'est pas installé",
@@ -847,8 +893,12 @@
"summary": "Résumé",
"poster": "Affiche",
"background": "Arrière-plan",
"logo": "Logo",
"squareArt": "Image carrée",
"selectPoster": "Sélectionner l'affiche",
"selectBackground": "Sélectionner l'arrière-plan",
"selectLogo": "Sélectionner le logo",
"selectSquareArt": "Sélectionner l'image carrée",
"fromUrl": "Depuis une URL",
"uploadFile": "Importer un fichier",
"enterImageUrl": "Entrer l'URL de l'image",
@@ -890,6 +940,39 @@
"subtitleMode": "Sélection automatique des sous-titres",
"manuallySelected": "Sélectionné manuellement",
"shownWithForeignAudio": "Affichés avec audio étranger",
"alwaysEnabled": "Toujours activé"
"alwaysEnabled": "Toujours activé",
"tags": "Tags",
"addTag": "Ajouter un tag",
"genre": "Genre",
"director": "Réalisateur",
"writer": "Scénariste",
"producer": "Producteur",
"country": "Pays",
"collection": "Collection",
"label": "Label",
"style": "Style",
"mood": "Ambiance"
},
"serverTasks": {
"title": "Tâches du serveur",
"failedToLoad": "Échec du chargement des tâches",
"noTasks": "Aucune tâche en cours"
},
"trakt": {
"title": "Trakt",
"connected": "Connecté",
"connectedAs": "Connecté en tant que @${username}",
"disconnectConfirm": "Déconnecter le compte Trakt ?",
"disconnectConfirmBody": "Plezy n'enverra plus d'événements de lecture à Trakt. Vous pouvez vous reconnecter à tout moment.",
"scrobble": "Scrobbling en temps réel",
"scrobbleDescription": "Envoyer les événements de lecture, pause et arrêt à Trakt pendant la lecture.",
"watchedSync": "Synchroniser le statut « vu »",
"watchedSyncDescription": "Lorsque vous marquez un élément comme vu dans Plezy, il l'est aussi sur Trakt.",
"deviceCodeTitle": "Activer Plezy sur Trakt",
"deviceCodeBody": "Rendez-vous sur ${url} et entrez ce code :",
"openTraktActivate": "Ouvrir Trakt pour activer",
"waitingForAuthorization": "En attente d'autorisation…",
"codeCopied": "Code copié",
"connectFailed": "Échec de la connexion à Trakt. Réessayez."
}
}
+247 -164
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Accedi con Plex",
"showQRCode": "Mostra QR Code",
"authenticate": "Autenticazione",
"debugEnterToken": "Debug: Inserisci Token Plex",
"plexTokenLabel": "Token Auth Plex",
"plexTokenHint": "Inserisci il tuo token di Plex.tv",
"authenticationTimeout": "Autenticazione scaduta. Riprova.",
"scanQRToSignIn": "Scansiona il QR code per accedere",
"waitingForAuth": "In attesa di autenticazione...\nCompleta l'accesso dal tuo browser.",
@@ -32,8 +29,7 @@
"delete": "Elimina",
"shuffle": "Casuale",
"addTo": "Aggiungi a...",
"remove": "Rimuovi",
"paste": "Incolla",
"createNew": "Crea",
"connect": "Connetti",
"disconnect": "Disconnetti",
"play": "Riproduci",
@@ -43,10 +39,9 @@
"search": "Cerca",
"home": "Home",
"back": "Indietro",
"settings": "Impostazioni",
"settings": "Opzioni",
"mute": "Muto",
"ok": "OK",
"loading": "Caricamento...",
"reconnect": "Riconnetti",
"exitConfirmTitle": "Uscire dall'app?",
"exitConfirmMessage": "Sei sicuro di voler uscire?",
@@ -57,13 +52,14 @@
"refreshingServers": "Aggiornamento server...",
"loadingServers": "Caricamento server...",
"connectingToServers": "Connessione ai server...",
"startingOfflineMode": "Avvio modalità offline..."
"startingOfflineMode": "Avvio modalità offline...",
"loading": "Caricamento..."
},
"screens": {
"licenses": "Licenze",
"switchProfile": "Cambia profilo",
"subtitleStyling": "Stile sottotitoli",
"mpvConfig": "Configurazione MPV",
"mpvConfig": "mpv.conf",
"logs": "Registro"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Impostazioni",
"supportDeveloper": "Supporta Plezy",
"supportDeveloperDescription": "Dona tramite Liberapay per finanziare lo sviluppo",
"language": "Lingua",
"theme": "Tema",
"appearance": "Aspetto",
"videoPlayback": "Riproduzione video",
"videoPlaybackDescription": "Configura il comportamento di riproduzione",
"advanced": "Avanzate",
"episodePosterMode": "Stile poster episodio",
"seriesPoster": "Poster della serie",
"seriesPosterDescription": "Mostra il poster della serie per tutti gli episodi",
"seasonPoster": "Poster della stagione",
"seasonPosterDescription": "Mostra il poster specifico della stagione per gli episodi",
"episodeThumbnail": "Miniatura episodio",
"episodeThumbnailDescription": "Mostra miniature 16:9 degli episodi",
"episodeThumbnail": "Miniatura",
"showHeroSectionDescription": "Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale",
"secondsLabel": "Secondi",
"minutesLabel": "Minuti",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Inserisci durata (${min}-${max})",
"systemTheme": "Sistema",
"systemThemeDescription": "Segui le impostazioni di sistema",
"lightTheme": "Chiaro",
"darkTheme": "Scuro",
"oledTheme": "OLED",
"oledThemeDescription": "Nero puro per schermi OLED",
"libraryDensity": "Densità libreria",
"compact": "Compatta",
"compactDescription": "Schede più piccole, più elementi visibili",
"normal": "Normale",
"normalDescription": "Dimensione predefinita",
"comfortable": "Comoda",
"comfortableDescription": "Schede più grandi, meno elementi visibili",
"viewMode": "Modalità di visualizzazione",
"gridView": "Griglia",
"gridViewDescription": "Visualizza gli elementi in un layout a griglia",
"listView": "Elenco",
"listViewDescription": "Visualizza gli elementi in un layout a elenco",
"showHeroSection": "Mostra sezione principale",
"useGlobalHubs": "Usa layout Home di Plex",
"useGlobalHubsDescription": "Mostra gli hub della home page come il client Plex ufficiale. Se disattivato, mostra invece i suggerimenti per libreria.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Sfoca le miniature e nascondi le descrizioni degli episodi che non hai ancora guardato",
"playerBackend": "Motore di riproduzione",
"exoPlayer": "ExoPlayer (Consigliato)",
"exoPlayerDescription": "Lettore nativo Android con migliore supporto hardware",
"mpv": "MPV",
"mpvDescription": "Lettore avanzato con più funzionalità e supporto sottotitoli ASS",
"mpv": "mpv",
"hardwareDecoding": "Decodifica Hardware",
"hardwareDecodingDescription": "Utilizza l'accelerazione hardware quando disponibile",
"bufferSize": "Dimensione buffer",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Personalizza l'aspetto dei sottotitoli",
"smallSkipDuration": "Durata skip breve",
"largeSkipDuration": "Durata skip lungo",
"rewindOnResume": "Riavvolgi alla ripresa",
"secondsUnit": "${seconds} secondi",
"defaultSleepTimer": "Timer spegnimento predefinito",
"minutesUnit": "${minutes} minuti",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Personalizza le scorciatoie da tastiera",
"videoPlayerNavigation": "Navigazione del lettore video",
"videoPlayerNavigationDescription": "Usa i tasti freccia per navigare nei controlli del lettore video",
"watchTogetherRelay": "Relay Guarda Insieme",
"watchTogetherRelayDescription": "Imposta un server relay personalizzato per Guarda Insieme. Tutti i partecipanti devono utilizzare lo stesso server.",
"watchTogetherRelayHint": "https://mio-relay.esempio.it",
"crashReporting": "Segnalazione errori",
"crashReportingDescription": "Invia segnalazioni di errori per migliorare l'app",
"debugLogging": "Log di debug",
"debugLoggingDescription": "Abilita il logging dettagliato per la risoluzione dei problemi",
"viewLogs": "Visualizza log",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Salta automaticamente i crediti e riproduci l'episodio successivo",
"autoSkipDelay": "Ritardo Salto Automatico",
"autoSkipDelayDescription": "Aspetta ${seconds} secondi prima del salto automatico",
"introPattern": "Pattern marcatore intro",
"introPatternDescription": "Espressione regolare per riconoscere i marcatori intro nei titoli dei capitoli",
"creditsPattern": "Pattern marcatore titoli di coda",
"creditsPatternDescription": "Espressione regolare per riconoscere i marcatori dei titoli di coda nei capitoli",
"invalidRegex": "Espressione regolare non valida",
"downloads": "Download",
"downloadLocationDescription": "Scegli dove salvare i contenuti scaricati",
"downloadLocationDefault": "Predefinita (Archiviazione App)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Impossibile selezionare la cartella",
"downloadOnWifiOnly": "Scarica solo con WiFi",
"downloadOnWifiOnlyDescription": "Impedisci i download quando si utilizza la rete dati cellulare",
"autoRemoveWatchedDownloads": "Rimuovi automaticamente i download visti",
"autoRemoveWatchedDownloadsDescription": "Elimina automaticamente episodi e film scaricati quando vengono segnati come visti",
"cellularDownloadBlocked": "I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l'impostazione.",
"maxVolume": "Volume massimo",
"maxVolumeDescription": "Consenti volume superiore al 100% per contenuti audio bassi",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Mostra su Discord cosa stai guardando",
"trakt": "Trakt",
"traktDescription": "Sincronizza la cronologia di visione con Trakt",
"companionRemoteServer": "Server telecomando",
"companionRemoteServerDescription": "Consenti ai dispositivi mobili nella tua rete di controllare questa app",
"autoPip": "Picture-in-Picture automatico",
"autoPipDescription": "Attiva automaticamente il picture-in-picture quando si esce dall'app durante la riproduzione",
"matchContentFrameRate": "Adatta frequenza fotogrammi",
"matchContentFrameRateDescription": "Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Adatta frequenza di aggiornamento",
"matchRefreshRateDescription": "Cambia la frequenza di aggiornamento del display per adattarla al contenuto video a schermo intero",
"matchDynamicRange": "Adatta gamma dinamica",
"matchDynamicRangeDescription": "Attiva automaticamente l'HDR per i contenuti HDR e torna in SDR all'uscita dal lettore",
"displaySwitchDelay": "Ritardo cambio display",
"tunneledPlayback": "Riproduzione tunnelizzata",
"tunneledPlaybackDescription": "Usa il tunneling video accelerato dall'hardware. Disattiva se vedi uno schermo nero con audio su contenuti HDR",
"requireProfileSelectionOnOpen": "Chiedi profilo all'apertura",
"requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta",
"confirmExitOnBack": "Conferma prima di uscire",
"confirmExitOnBackDescription": "Mostra una finestra di conferma quando si preme indietro per uscire dall'app"
"confirmExitOnBackDescription": "Mostra una finestra di conferma quando si preme indietro per uscire dall'app",
"forceTvMode": "Forza modalità TV",
"forceTvModeDescription": "Usa il layout TV indipendentemente dal rilevamento automatico. Utile sui dispositivi Android TV che non segnalano la funzionalità leanback. Riavvia l'app al cambiamento.",
"autoHidePerformanceOverlay": "Nascondi automaticamente overlay prestazioni",
"autoHidePerformanceOverlayDescription": "Dissolvi l'overlay prestazioni con i controlli di riproduzione",
"showNavBarLabels": "Mostra etichette barra di navigazione",
"showNavBarLabelsDescription": "Mostra le etichette sotto le icone della barra di navigazione",
"liveTvDefaultFavorites": "Canali preferiti predefiniti",
"liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti all'apertura della TV dal vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Cerca film. spettacoli, musica...",
@@ -239,11 +263,6 @@
"skipMarker": "Salta intro/titoli di coda"
}
},
"pinEntry": {
"enterPin": "Inserisci PIN",
"showPin": "Mostra PIN",
"hidePin": "Nascondi PIN"
},
"fileInfo": {
"title": "Info sul file",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Colori primari",
"chromaSubsampling": "Sottocampionamento cromatico",
"channels": "Canali",
"subtitles": "Sottotitoli",
"overallBitrate": "Bitrate complessivo",
"path": "Percorso",
"size": "Dimensione",
"container": "Contenitore",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Questo include tutti gli episodi e i loro file.",
"mediaDeletedSuccessfully": "Elemento multimediale eliminato con successo",
"mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale",
"rate": "Valuta"
"rate": "Valuta",
"playFromBeginning": "Riproduci dall'inizio",
"playVersion": "Riproduci versione..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
@@ -316,7 +339,6 @@
"unlockRotation": "Sblocca rotazione",
"timerActive": "Timer attivo",
"playbackWillPauseIn": "La riproduzione si interromperà tra ${duration}",
"sleepTimerCompleted": "Timer di spegnimento completato - riproduzione in pausa",
"stillWatching": "Stai ancora guardando?",
"pausingIn": "Pausa tra ${seconds}s",
"continueWatching": "Continua",
@@ -333,34 +355,43 @@
"muteButton": "Silenzia",
"unmuteButton": "Riattiva audio",
"settingsButton": "Impostazioni video",
"audioTrackButton": "Tracce audio",
"subtitlesButton": "Sottotitoli",
"tracksButton": "Audio e sottotitoli",
"chaptersButton": "Capitoli",
"versionsButton": "Versioni video",
"pipButton": "Modalità Picture-in-Picture",
"aspectRatioButton": "Proporzioni",
"ambientLighting": "Illuminazione ambientale",
"ambientLightingOn": "Attiva illuminazione ambientale",
"ambientLightingOff": "Disattiva illuminazione ambientale",
"fullscreenButton": "Attiva schermo intero",
"exitFullscreenButton": "Esci da schermo intero",
"alwaysOnTopButton": "Sempre in primo piano",
"rotationLockButton": "Blocco rotazione",
"lockScreen": "Blocca schermo",
"screenLockButton": "Blocco schermo",
"longPressToUnlock": "Premi a lungo per sbloccare",
"timelineSlider": "Timeline video",
"volumeSlider": "Livello volume",
"endsAt": "Finisce alle ${time}",
"pipActive": "Riproduzione in Picture-in-Picture",
"pipFailed": "Impossibile avviare la modalità Picture-in-Picture",
"pipErrors": {
"androidVersion": "Richiede Android 8.0 o versioni successive",
"iosVersion": "Richiede iOS 15.0 o versioni successive",
"permissionDisabled": "L'autorizzazione Picture-in-Picture è disabilitata. Abilitala in Impostazioni > App > Plezy > Picture-in-Picture",
"notSupported": "Questo dispositivo non supporta la modalità Picture-in-Picture",
"voSwitchFailed": "Impossibile cambiare l'uscita video per Picture-in-Picture",
"failed": "Impossibile avviare la modalità Picture-in-Picture",
"unknown": "Si è verificato un errore: ${error}"
},
"chapters": "Capitoli",
"noChaptersAvailable": "Nessun capitolo disponibile",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "Coda",
"noQueueItems": "Nessun elemento in coda",
"searchSubtitles": "Cerca sottotitoli",
"language": "Lingua",
"noSubtitlesFound": "Nessun sottotitolo trovato",
"subtitleDownloaded": "Sottotitolo scaricato",
"subtitleDownloadFailed": "Impossibile scaricare il sottotitolo",
"searchLanguages": "Cerca lingue..."
},
"userStatus": {
"admin": "Admin",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Segna come non visto",
"markedAsWatchedOffline": "Segnato come visto (sincronizzato online)",
"markedAsUnwatchedOffline": "Segnato come non visto (sincronizzato online)",
"autoRemovedWatchedDownload": "Rimosso automaticamente: ${title}",
"removedFromContinueWatching": "Rimosso da Continua a guardare",
"errorLoading": "Errore: ${error}",
"fileInfoNotAvailable": "Informazioni sul file non disponibili",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Impossibile creare la coda di riproduzione - nessun elemento",
"failedPlayback": "Impossibile ${action}: ${error}",
"switchingToCompatiblePlayer": "Passaggio al lettore compatibile...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Riproduzione non riuscita",
"serverLimitBody": "Il server ha restituito un errore (HTTP 500). Di solito significa che il proprietario del server ha impostato un limite di banda o di transcodifica che sta rifiutando la tua sessione. Non c'è nulla da fare dal client — il proprietario del server deve modificare le sue impostazioni.",
"logsUploaded": "Log caricati",
"logsUploadFailed": "Caricamento log fallito",
"logId": "ID log"
},
"subtitlingStyling": {
"stylingOptions": "Opzioni stile",
"text": "Testo",
"border": "Bordo",
"background": "Sfondo",
"fontSize": "Dimensione",
"textColor": "Colore testo",
"borderSize": "Dimensione bordo",
"borderColor": "Colore bordo",
"backgroundOpacity": "Opacità sfondo",
"backgroundColor": "Colore sfondo",
"position": "Position"
"position": "Posizione",
"assOverride": "Sovrascrittura ASS",
"bold": "Grassetto",
"italic": "Corsivo"
},
"mpvConfig": {
"title": "Configurazione MPV",
"title": "Configurazione mpv",
"description": "Impostazioni avanzate del lettore video",
"properties": "Proprietà",
"presets": "Preset",
"noProperties": "Nessuna proprietà configurata",
"noPresets": "Nessun preset salvato",
"addProperty": "Aggiungi proprietà",
"editProperty": "Modifica proprietà",
"deleteProperty": "Elimina proprietà",
"propertyKey": "Chiave proprietà",
"propertyKeyHint": "es. hwdec, demuxer-max-bytes",
"propertyValue": "Valore proprietà",
"propertyValueHint": "es. auto, 256000000",
"saveAsPreset": "Salva come preset...",
"presetName": "Nome preset",
"presetNameHint": "Inserisci un nome per questo preset",
@@ -436,8 +466,7 @@
"presetLoaded": "Preset caricato",
"presetDeleted": "Preset eliminato",
"confirmDeletePreset": "Sei sicuro di voler eliminare questo preset?",
"confirmDeleteProperty": "Sei sicuro di voler eliminare questa proprietà?",
"entriesCount": "${count} voci"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Conferma azione"
@@ -452,11 +481,8 @@
"overview": "Panoramica",
"cast": "Attori",
"extras": "Trailer ed Extra",
"seasons": "Stagioni",
"studio": "Studio",
"rating": "Classificazione",
"episodeCount": "${count} episodi",
"watchedProgress": "${watched}/${total} guardati",
"movie": "Film",
"tvShow": "Serie TV",
"minutesLeft": "${minutes} minuti rimanenti"
@@ -518,6 +544,7 @@
"playlists": "Playlist"
},
"groupings": {
"title": "Raggruppamento",
"all": "Tutti",
"movies": "Film",
"shows": "Serie TV",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Cancella log",
"copyLogs": "Copia log",
"uploadLogs": "Upload Logs",
"error": "Errore:",
"stackTrace": "Traccia dello stack:"
"uploadLogs": "Carica log"
},
"licenses": {
"relatedPackages": "Pacchetti correlati",
@@ -561,24 +586,16 @@
"navigation": {
"libraries": "Librerie",
"downloads": "Download",
"liveTv": "TV in diretta"
"liveTv": "TV Live"
},
"liveTv": {
"title": "TV in diretta",
"channels": "Canali",
"guide": "Guida",
"noChannels": "Nessun canale disponibile",
"noDvr": "Nessun DVR configurato su nessun server",
"tuneFailed": "Impossibile sintonizzare il canale",
"loading": "Caricamento canali...",
"nowPlaying": "In riproduzione",
"noPrograms": "Nessun dato di programma disponibile",
"channelNumber": "Canale ${number}",
"live": "IN DIRETTA",
"hd": "HD",
"premiere": "NUOVO",
"reloadGuide": "Ricarica guida",
"allChannels": "Tutti i canali",
"now": "Ora",
"today": "Oggi",
"midnight": "Mezzanotte",
@@ -588,30 +605,41 @@
"evening": "Sera",
"lateNight": "Notte tarda",
"whatsOn": "In onda ora",
"watchChannel": "Guarda canale"
"watchChannel": "Guarda canale",
"favorites": "Preferiti",
"reorderFavorites": "Riordina preferiti",
"joinSession": "Partecipa alla sessione in corso",
"watchFromStart": "Guarda dall'inizio (${minutes} min fa)",
"watchLive": "Guarda in diretta",
"goToLive": "Vai al live"
},
"downloads": {
"title": "Download",
"manage": "Gestisci",
"tvShows": "Serie TV",
"movies": "Film",
"noDownloads": "Nessun download",
"noDownloadsDescription": "I contenuti scaricati appariranno qui per la visualizzazione offline",
"downloadNow": "Scarica",
"deleteDownload": "Elimina download",
"retryDownload": "Riprova download",
"downloadQueued": "Download in coda",
"episodesQueued": "${count} episodi in coda per il download",
"downloadDeleted": "Download eliminato",
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Il file scaricato verrà rimosso dal tuo dispositivo.",
"deletingWithProgress": "Eliminazione di ${title}... (${current} di ${total})",
"noDownloadsTree": "Nessun download",
"pauseAll": "Metti tutto in pausa",
"resumeAll": "Riprendi tutto",
"deleteAll": "Elimina tutto"
"collections": {
"title": "Raccolte",
"collection": "Raccolta",
"empty": "La raccolta è vuota",
"unknownLibrarySection": "Impossibile eliminare: sezione libreria sconosciuta",
"deleteCollection": "Elimina raccolta",
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Questa azione non può essere annullata.",
"deleted": "Raccolta eliminata",
"deleteFailed": "Impossibile eliminare la raccolta",
"deleteFailedWithError": "Impossibile eliminare la raccolta: ${error}",
"failedToLoadItems": "Impossibile caricare gli elementi della raccolta: ${error}",
"selectCollection": "Seleziona raccolta",
"collectionName": "Nome raccolta",
"enterCollectionName": "Inserisci nome raccolta",
"addedToCollection": "Aggiunto alla raccolta",
"errorAddingToCollection": "Errore nell'aggiunta alla raccolta",
"created": "Raccolta creata",
"removeFromCollection": "Rimuovi dalla raccolta",
"removeFromCollectionConfirm": "Rimuovere \"${title}\" da questa raccolta?",
"removedFromCollection": "Rimosso dalla raccolta",
"removeFromCollectionFailed": "Impossibile rimuovere dalla raccolta",
"removeFromCollectionError": "Errore durante la rimozione dalla raccolta: ${error}",
"searchCollections": "Cerca raccolte..."
},
"playlists": {
"title": "Playlist",
"playlist": "Playlist",
"noPlaylists": "Nessuna playlist trovata",
"create": "Crea playlist",
"playlistName": "Nome playlist",
@@ -629,38 +657,12 @@
"itemAdded": "Aggiunto alla playlist",
"itemRemoved": "Rimosso dalla playlist",
"selectPlaylist": "Seleziona playlist",
"createNewPlaylist": "Crea nuova playlist",
"errorCreating": "Errore durante la creazione della playlist",
"errorDeleting": "Errore durante l'eliminazione della playlist",
"errorLoading": "Errore durante il caricamento delle playlist",
"errorAdding": "Errore durante l'aggiunta alla playlist",
"errorReordering": "Errore durante il riordino dell'elemento della playlist",
"errorRemoving": "Errore durante la rimozione dalla playlist",
"playlist": "Playlist"
},
"collections": {
"title": "Raccolte",
"collection": "Raccolta",
"empty": "La raccolta è vuota",
"unknownLibrarySection": "Impossibile eliminare: sezione libreria sconosciuta",
"deleteCollection": "Elimina raccolta",
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Questa azione non può essere annullata.",
"deleted": "Raccolta eliminata",
"deleteFailed": "Impossibile eliminare la raccolta",
"deleteFailedWithError": "Impossibile eliminare la raccolta: ${error}",
"failedToLoadItems": "Impossibile caricare gli elementi della raccolta: ${error}",
"selectCollection": "Seleziona raccolta",
"createNewCollection": "Crea nuova raccolta",
"collectionName": "Nome raccolta",
"enterCollectionName": "Inserisci nome raccolta",
"addedToCollection": "Aggiunto alla raccolta",
"errorAddingToCollection": "Errore nell'aggiunta alla raccolta",
"created": "Raccolta creata",
"removeFromCollection": "Rimuovi dalla raccolta",
"removeFromCollectionConfirm": "Rimuovere \"${title}\" da questa raccolta?",
"removedFromCollection": "Rimosso dalla raccolta",
"removeFromCollectionFailed": "Impossibile rimuovere dalla raccolta",
"removeFromCollectionError": "Errore durante la rimozione dalla raccolta: ${error}"
"errorRemoving": "Errore durante la rimozione dalla playlist"
},
"watchTogether": {
"title": "Guarda Insieme",
@@ -697,18 +699,76 @@
"leave": "Lascia",
"syncing": "Sincronizzazione...",
"joinWatchSession": "Unisciti alla Sessione di Visione",
"enterCodeHint": "Inserisci codice di 8 caratteri",
"enterCodeHint": "Inserisci codice di 5 caratteri",
"pasteFromClipboard": "Incolla dagli appunti",
"pleaseEnterCode": "Inserisci un codice sessione",
"codeMustBe8Chars": "Il codice sessione deve essere di 8 caratteri",
"codeMustBe5Chars": "Il codice sessione deve essere di 5 caratteri",
"joinInstructions": "Inserisci il codice sessione condiviso dall'host per unirti alla loro sessione di visione.",
"failedToCreate": "Impossibile creare la sessione",
"failedToJoin": "Impossibile unirsi alla sessione",
"sessionCodeCopied": "Codice sessione copiato negli appunti",
"relayUnreachable": "Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.",
"reconnectingToHost": "Riconnessione all'host...",
"currentPlayback": "Riproduzione corrente",
"joinCurrentPlayback": "Unisciti alla riproduzione corrente",
"joinCurrentPlaybackDescription": "Torna a ciò che l'host sta guardando in questo momento",
"failedToOpenCurrentPlayback": "Impossibile aprire la riproduzione corrente",
"participantJoined": "${name} si è unito",
"participantLeft": "${name} se ne è andato"
"participantLeft": "${name} se ne è andato",
"participantPaused": "${name} ha messo in pausa",
"participantResumed": "${name} ha ripreso",
"participantSeeked": "${name} ha cercato",
"participantBuffering": "${name} sta caricando",
"waitingForParticipants": "In attesa che gli altri carichino...",
"recentRooms": "Stanze recenti",
"renameRoom": "Rinomina stanza",
"removeRoom": "Rimuovi"
},
"downloads": {
"title": "Download",
"manage": "Gestisci",
"tvShows": "Serie TV",
"movies": "Film",
"noDownloads": "Nessun download",
"noDownloadsDescription": "I contenuti scaricati appariranno qui per la visualizzazione offline",
"downloadNow": "Scarica",
"deleteDownload": "Elimina download",
"retryDownload": "Riprova download",
"downloadQueued": "Download in coda",
"serverErrorBitrate": "Errore del server — il file potrebbe superare il limite di bitrate dello streaming remoto",
"episodesQueued": "${count} episodi in coda per il download",
"downloadDeleted": "Download eliminato",
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Il file scaricato verrà rimosso dal tuo dispositivo.",
"deletingWithProgress": "Eliminazione di ${title}... (${current} di ${total})",
"noDownloadsTree": "Nessun download",
"pauseAll": "Metti tutto in pausa",
"resumeAll": "Riprendi tutto",
"deleteAll": "Elimina tutto",
"selectVersion": "Seleziona versione",
"allEpisodes": "Tutti gli episodi",
"unwatchedOnly": "Solo non visti",
"nextNUnwatched": "Prossimi ${count} non visti",
"customAmount": "Quantità personalizzata...",
"howManyEpisodes": "Quanti episodi?",
"itemsQueued": "${count} elementi in coda per il download",
"keepSynced": "Mantieni sincronizzato",
"downloadOnce": "Scarica una volta",
"keepNUnwatched": "Mantieni ${count} non visti",
"editSyncRule": "Modifica regola di sincronizzazione",
"removeSyncRule": "Rimuovi regola di sincronizzazione",
"removeSyncRuleConfirm": "Interrompere la sincronizzazione di \"${title}\"? Gli episodi scaricati verranno mantenuti.",
"syncRuleCreated": "Regola di sincronizzazione creata — ${count} episodi non visti mantenuti",
"syncRuleUpdated": "Regola di sincronizzazione aggiornata",
"syncRuleRemoved": "Regola di sincronizzazione rimossa",
"syncedNewEpisodes": "${count} nuovi episodi sincronizzati per ${title}",
"activeSyncRules": "Regole di sincronizzazione",
"noSyncRules": "Nessuna regola di sincronizzazione",
"manageSyncRule": "Gestisci sincronizzazione",
"editEpisodeCount": "Numero di episodi",
"editSyncFilter": "Filtro di sincronizzazione",
"syncAllItems": "Sincronizzazione di tutti gli elementi",
"syncUnwatchedItems": "Sincronizzazione degli elementi non visti",
"syncRuleListCreated": "Regola di sincronizzazione creata"
},
"shaders": {
"title": "Shader",
@@ -716,58 +776,46 @@
"nvscalerDescription": "Ridimensionamento NVIDIA per video più nitido",
"qualityFast": "Veloce",
"qualityHQ": "Alta qualità",
"mode": "Modalità"
"mode": "Modalità",
"importShader": "Importa shader",
"customShaderDescription": "Shader GLSL personalizzato",
"shaderImported": "Shader importato",
"shaderImportFailed": "Importazione shader fallita",
"deleteShader": "Elimina shader",
"deleteShaderConfirm": "Eliminare \"${name}\"?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Connetti a un dispositivo",
"hostRemoteSession": "Ospita sessione remota",
"controlThisDevice": "Controlla questo dispositivo con il tuo telefono",
"remoteControl": "Telecomando",
"controlDesktop": "Controlla un dispositivo desktop",
"title": "Telecomando",
"connectedTo": "Connesso a ${name}",
"session": {
"creatingSession": "Creazione sessione remota...",
"failedToCreate": "Impossibile creare la sessione remota:",
"noSession": "Nessuna sessione disponibile",
"scanQrCode": "Scansiona QR Code",
"orEnterManually": "Oppure inserisci manualmente",
"startingServer": "Avvio del server remoto...",
"failedToCreate": "Impossibile avviare il server remoto:",
"hostAddress": "Indirizzo host",
"sessionId": "ID sessione",
"pin": "PIN",
"connected": "Connesso",
"waitingForConnection": "In attesa di connessione...",
"serverRunning": "Server remoto attivo",
"serverStopped": "Server remoto arrestato",
"serverRunningDescription": "I dispositivi mobili nella tua rete possono scoprire e connettersi a questa app",
"serverStoppedDescription": "Avvia il server per consentire ai dispositivi mobili di connettersi",
"usePhoneToControl": "Usa il tuo dispositivo mobile per controllare questa app",
"copiedToClipboard": "${label} copiato negli appunti",
"copyToClipboard": "Copia negli appunti",
"newSession": "Nuova sessione",
"startServer": "Avvia server",
"stopServer": "Arresta server",
"minimize": "Riduci"
},
"pairing": {
"scan": "Scansiona",
"manual": "Manuale",
"pairWithDesktop": "Associa con desktop",
"enterSessionDetails": "Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop",
"discoveryDescription": "I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Inserisci ID sessione di 8 caratteri",
"pinHint": "Inserisci PIN di 6 cifre",
"connecting": "Connessione...",
"tips": "Suggerimenti",
"tipDesktop": "Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu",
"tipScan": "Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop",
"tipWifi": "Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi",
"cameraPermissionRequired": "L'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l'accesso alla fotocamera nelle impostazioni del dispositivo.",
"cameraError": "Impossibile avviare la fotocamera: ${error}",
"scanInstruction": "Punta la fotocamera verso il QR code mostrato sul tuo desktop",
"invalidQrCode": "Formato QR code non valido",
"searchingForDevices": "Ricerca dispositivi...",
"noDevicesFound": "Nessun dispositivo trovato nella tua rete",
"noDevicesHint": "Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi",
"availableDevices": "Dispositivi disponibili",
"manualConnection": "Connessione manuale",
"cryptoInitFailed": "Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l'accesso a un account Plex.",
"validationHostRequired": "Inserisci l'indirizzo host",
"validationHostFormat": "Il formato deve essere IP:porta (es. 192.168.1.100:48632)",
"validationSessionIdRequired": "Inserisci un ID sessione",
"validationSessionIdLength": "L'ID sessione deve essere di 8 caratteri",
"validationPinRequired": "Inserisci un PIN",
"validationPinLength": "Il PIN deve essere di 6 cifre",
"connectionTimedOut": "Connessione scaduta. Verifica l'ID sessione e il PIN.",
"sessionNotFound": "Sessione non trovata. Verifica le tue credenziali.",
"connectionTimedOut": "Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.",
"sessionNotFound": "Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull'host.",
"authFailed": "Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.",
"failedToConnect": "Connessione fallita: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Riconnessione...",
"attemptOf": "Tentativo ${current} di 5",
"retryNow": "Riprova ora",
"connectionError": "Errore di connessione",
"notConnected": "Non connesso",
"tabRemote": "Telecomando",
"tabPlay": "Riproduci",
"tabMore": "Altro",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Usa lettore esterno",
"useExternalPlayerDescription": "Apri i video in un'app esterna invece del lettore integrato",
"selectPlayer": "Seleziona lettore",
"customPlayers": "Lettori personalizzati",
"systemDefault": "Predefinito di sistema",
"addCustomPlayer": "Aggiungi lettore personalizzato",
"playerName": "Nome lettore",
"playerCommand": "Comando",
"playerPackage": "Nome pacchetto",
"playerUrlScheme": "Schema URL",
"customPlayer": "Lettore personalizzato",
"off": "Disattivato",
"launchFailed": "Impossibile aprire il lettore esterno",
"appNotInstalled": "${name} non è installato",
@@ -847,8 +893,12 @@
"summary": "Trama",
"poster": "Poster",
"background": "Sfondo",
"logo": "Logo",
"squareArt": "Immagine quadrata",
"selectPoster": "Seleziona poster",
"selectBackground": "Seleziona sfondo",
"selectLogo": "Seleziona logo",
"selectSquareArt": "Seleziona immagine quadrata",
"fromUrl": "Da URL",
"uploadFile": "Carica file",
"enterImageUrl": "Inserisci URL immagine",
@@ -890,6 +940,39 @@
"subtitleMode": "Selezione automatica sottotitoli",
"manuallySelected": "Selezionato manualmente",
"shownWithForeignAudio": "Mostrati con audio straniero",
"alwaysEnabled": "Sempre attivo"
"alwaysEnabled": "Sempre attivo",
"tags": "Tag",
"addTag": "Aggiungi tag",
"genre": "Genere",
"director": "Regista",
"writer": "Sceneggiatore",
"producer": "Produttore",
"country": "Paese",
"collection": "Collezione",
"label": "Etichetta",
"style": "Stile",
"mood": "Atmosfera"
},
"serverTasks": {
"title": "Attività del server",
"failedToLoad": "Impossibile caricare le attività",
"noTasks": "Nessuna attività in corso"
},
"trakt": {
"title": "Trakt",
"connected": "Connesso",
"connectedAs": "Connesso come @${username}",
"disconnectConfirm": "Disconnettere l'account Trakt?",
"disconnectConfirmBody": "Plezy smetterà di inviare eventi di riproduzione a Trakt. Puoi riconnetterti in qualsiasi momento.",
"scrobble": "Scrobbling in tempo reale",
"scrobbleDescription": "Invia eventi di riproduzione, pausa e arresto a Trakt durante la riproduzione.",
"watchedSync": "Sincronizza stato visualizzato",
"watchedSyncDescription": "Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt.",
"deviceCodeTitle": "Attiva Plezy su Trakt",
"deviceCodeBody": "Visita ${url} e inserisci questo codice:",
"openTraktActivate": "Apri Trakt per attivare",
"waitingForAuthorization": "In attesa di autorizzazione…",
"codeCopied": "Codice copiato",
"connectFailed": "Impossibile connettersi a Trakt. Riprova."
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Plexでサインイン",
"showQRCode": "QRコードを表示",
"authenticate": "認証",
"authenticationTimeout": "認証がタイムアウトしました。もう一度お試しください。",
"scanQRToSignIn": "このQRコードをスキャンしてサインイン",
"waitingForAuth": "認証を待機中...\nブラウザでサインインを完了してください。",
"useBrowser": "ブラウザを使用"
},
"common": {
"cancel": "キャンセル",
"save": "保存",
"close": "閉じる",
"clear": "クリア",
"reset": "リセット",
"later": "後で",
"submit": "送信",
"confirm": "確認",
"retry": "再試行",
"logout": "ログアウト",
"unknown": "不明",
"refresh": "更新",
"yes": "はい",
"no": "いいえ",
"delete": "削除",
"shuffle": "シャッフル",
"addTo": "追加...",
"createNew": "新規作成",
"connect": "接続",
"disconnect": "切断",
"play": "再生",
"pause": "一時停止",
"resume": "再開",
"error": "エラー",
"search": "検索",
"home": "ホーム",
"back": "戻る",
"settings": "設定",
"mute": "ミュート",
"ok": "OK",
"reconnect": "再接続",
"exitConfirmTitle": "アプリを終了しますか?",
"exitConfirmMessage": "終了してもよろしいですか?",
"dontAskAgain": "次回から表示しない",
"exit": "終了",
"viewAll": "すべて表示",
"checkingNetwork": "ネットワークを確認中...",
"refreshingServers": "サーバーを更新中...",
"loadingServers": "サーバーを読み込み中...",
"connectingToServers": "サーバーに接続中...",
"startingOfflineMode": "オフラインモードを開始中...",
"loading": "読み込み中..."
},
"screens": {
"licenses": "ライセンス",
"switchProfile": "プロフィール切替",
"subtitleStyling": "字幕スタイル",
"mpvConfig": "mpv.conf",
"logs": "ログ"
},
"update": {
"available": "アップデート利用可能",
"versionAvailable": "バージョン ${version} が利用可能です",
"currentVersion": "現在: ${version}",
"skipVersion": "このバージョンをスキップ",
"viewRelease": "リリースを表示",
"latestVersion": "最新バージョンです",
"checkFailed": "アップデートの確認に失敗しました"
},
"settings": {
"title": "設定",
"supportDeveloper": "Plezy を支援",
"supportDeveloperDescription": "Liberapay で寄付して開発を支援",
"language": "言語",
"theme": "テーマ",
"appearance": "外観",
"videoPlayback": "動画再生",
"videoPlaybackDescription": "再生動作を設定",
"advanced": "詳細",
"episodePosterMode": "エピソードポスタースタイル",
"seriesPoster": "シリーズポスター",
"seasonPoster": "シーズンポスター",
"episodeThumbnail": "サムネイル",
"showHeroSectionDescription": "ホーム画面に注目コンテンツのカルーセルを表示",
"secondsLabel": "秒",
"minutesLabel": "分",
"secondsShort": "秒",
"minutesShort": "分",
"durationHint": "時間を入力 (${min}-${max})",
"systemTheme": "システム",
"lightTheme": "ライト",
"darkTheme": "ダーク",
"oledTheme": "OLED",
"libraryDensity": "ライブラリの密度",
"compact": "コンパクト",
"comfortable": "ゆったり",
"viewMode": "表示モード",
"gridView": "グリッド",
"listView": "リスト",
"showHeroSection": "ヒーローセクションを表示",
"useGlobalHubs": "Plex Homeレイアウトを使用",
"useGlobalHubsDescription": "公式Plexクライアントのようにホームページのハブを表示。オフにすると、ライブラリごとのおすすめを表示。",
"showServerNameOnHubs": "ハブにサーバー名を表示",
"showServerNameOnHubsDescription": "ハブタイトルに常にサーバー名を表示。オフにすると、重複名のみ表示。",
"alwaysKeepSidebarOpen": "サイドバーを常に開いておく",
"alwaysKeepSidebarOpenDescription": "サイドバーを展開したまま、コンテンツ領域が調整される",
"showUnwatchedCount": "未視聴数を表示",
"showUnwatchedCountDescription": "番組とシーズンに未視聴エピソード数を表示",
"hideSpoilers": "未視聴エピソードのネタバレを非表示",
"hideSpoilersDescription": "まだ視聴していないエピソードのサムネイルをぼかし、説明を非表示",
"playerBackend": "プレーヤーバックエンド",
"exoPlayer": "ExoPlayer(推奨)",
"mpv": "mpv",
"hardwareDecoding": "ハードウェアデコード",
"hardwareDecodingDescription": "利用可能な場合にハードウェアアクセラレーションを使用",
"bufferSize": "バッファサイズ",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "自動(推奨)",
"bufferSizeWarning": "デバイスのメモリは${heap}MBです。${size}MBのバッファは再生の問題を引き起こす可能性があります。",
"subtitleStyling": "字幕スタイル",
"subtitleStylingDescription": "字幕の外観をカスタマイズ",
"smallSkipDuration": "短いスキップ時間",
"largeSkipDuration": "長いスキップ時間",
"rewindOnResume": "再開時に巻き戻し",
"secondsUnit": "${seconds}秒",
"defaultSleepTimer": "デフォルトスリープタイマー",
"minutesUnit": "${minutes}分",
"rememberTrackSelections": "番組/映画ごとにトラック選択を記憶",
"rememberTrackSelectionsDescription": "再生中にトラックを変更すると、音声と字幕の言語設定を自動保存",
"clickVideoTogglesPlayback": "動画クリックで再生/一時停止を切替",
"clickVideoTogglesPlaybackDescription": "有効にすると、動画プレーヤーをクリックで再生/一時停止。それ以外は再生コントロールの表示/非表示。",
"videoPlayerControls": "動画プレーヤーコントロール",
"keyboardShortcuts": "キーボードショートカット",
"keyboardShortcutsDescription": "キーボードショートカットをカスタマイズ",
"videoPlayerNavigation": "動画プレーヤーナビゲーション",
"videoPlayerNavigationDescription": "矢印キーで動画プレーヤーコントロールを操作",
"watchTogetherRelay": "一緒に視聴リレーサーバー",
"watchTogetherRelayDescription": "一緒に視聴用のカスタムリレーサーバーを設定します。すべての参加者が同じサーバーを使用する必要があります。",
"watchTogetherRelayHint": "https://my-relay.example.com",
"crashReporting": "クラッシュレポート",
"crashReportingDescription": "アプリの改善に役立つクラッシュレポートを送信",
"debugLogging": "デバッグログ",
"debugLoggingDescription": "トラブルシューティング用の詳細なログを有効化",
"viewLogs": "ログを表示",
"viewLogsDescription": "アプリケーションログを表示",
"clearCache": "キャッシュをクリア",
"clearCacheDescription": "キャッシュされたすべての画像とデータをクリアします。クリア後、コンテンツの読み込みに時間がかかる場合があります。",
"clearCacheSuccess": "キャッシュを正常にクリアしました",
"resetSettings": "設定をリセット",
"resetSettingsDescription": "すべての設定をデフォルト値にリセットします。この操作は元に戻せません。",
"resetSettingsSuccess": "設定を正常にリセットしました",
"shortcutsReset": "ショートカットをデフォルトにリセットしました",
"about": "アプリについて",
"aboutDescription": "アプリ情報とライセンス",
"updates": "アップデート",
"updateAvailable": "アップデート利用可能",
"checkForUpdates": "アップデートを確認",
"validationErrorEnterNumber": "有効な数値を入力してください",
"validationErrorDuration": "時間は${min}から${max} ${unit}の間である必要があります",
"shortcutAlreadyAssigned": "ショートカットは既に${action}に割り当てられています",
"shortcutUpdated": "${action}のショートカットを更新しました",
"autoSkip": "自動スキップ",
"autoSkipIntro": "イントロを自動スキップ",
"autoSkipIntroDescription": "数秒後にイントロマーカーを自動的にスキップ",
"autoSkipCredits": "クレジットを自動スキップ",
"autoSkipCreditsDescription": "クレジットを自動的にスキップして次のエピソードを再生",
"autoSkipDelay": "自動スキップの遅延",
"autoSkipDelayDescription": "自動スキップまで${seconds}秒待機",
"introPattern": "イントロマーカーパターン",
"introPatternDescription": "チャプタータイトルのイントロマーカーに一致する正規表現パターン",
"creditsPattern": "クレジットマーカーパターン",
"creditsPatternDescription": "チャプタータイトルのクレジットマーカーに一致する正規表現パターン",
"invalidRegex": "無効な正規表現",
"downloads": "ダウンロード",
"downloadLocationDescription": "ダウンロードコンテンツの保存場所を選択",
"downloadLocationDefault": "デフォルト(アプリストレージ)",
"downloadLocationCustom": "カスタムの場所",
"selectFolder": "フォルダを選択",
"resetToDefault": "デフォルトに戻す",
"currentPath": "現在: ${path}",
"downloadLocationChanged": "ダウンロード場所を変更しました",
"downloadLocationReset": "ダウンロード場所をデフォルトにリセットしました",
"downloadLocationInvalid": "選択したフォルダは書き込みできません",
"downloadLocationSelectError": "フォルダの選択に失敗しました",
"downloadOnWifiOnly": "WiFiのみでダウンロード",
"downloadOnWifiOnlyDescription": "モバイルデータ通信時のダウンロードを防止",
"autoRemoveWatchedDownloads": "視聴済みダウンロードの自動削除",
"autoRemoveWatchedDownloadsDescription": "視聴済みにしたエピソードや映画のダウンロードを自動的に削除",
"cellularDownloadBlocked": "モバイルデータ通信ではダウンロードが無効です。WiFiに接続するか設定を変更してください。",
"maxVolume": "最大音量",
"maxVolumeDescription": "静かなメディアに対して100%以上の音量ブーストを許可",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Discordで視聴中の内容を表示",
"trakt": "Trakt",
"traktDescription": "視聴履歴を Trakt と同期",
"companionRemoteServer": "コンパニオンリモートサーバー",
"companionRemoteServerDescription": "ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする",
"autoPip": "自動ピクチャーインピクチャー",
"autoPipDescription": "再生中にアプリを離れると自動的にピクチャーインピクチャーに移行",
"matchContentFrameRate": "コンテンツのフレームレートに合わせる",
"matchContentFrameRateDescription": "動画コンテンツに合わせてディスプレイのリフレッシュレートを調整し、ジャダーを低減しバッテリーを節約",
"matchRefreshRate": "リフレッシュレートを合わせる",
"matchRefreshRateDescription": "フルスクリーン時にディスプレイのリフレッシュレートを動画コンテンツに合わせて切り替える",
"matchDynamicRange": "ダイナミックレンジを合わせる",
"matchDynamicRangeDescription": "HDRコンテンツに対して自動的にHDRを有効にし、プレーヤーを閉じるとSDRに戻す",
"displaySwitchDelay": "ディスプレイ切り替え遅延",
"tunneledPlayback": "トンネル再生",
"tunneledPlaybackDescription": "ハードウェアアクセラレーションされたビデオトンネリングを使用。HDRコンテンツで音声のみで画面が黒くなる場合は無効にしてください",
"requireProfileSelectionOnOpen": "アプリ起動時にプロフィールを確認",
"requireProfileSelectionOnOpenDescription": "アプリを開くたびにプロフィール選択を表示",
"confirmExitOnBack": "終了前に確認",
"confirmExitOnBackDescription": "戻るボタンでアプリを終了する際に確認ダイアログを表示",
"forceTvMode": "TVモードを強制",
"forceTvModeDescription": "自動検出に関係なくTVレイアウトを使用します。leanback機能を報告しないAndroid TVデバイスで便利です。変更するとアプリが再起動されます。",
"autoHidePerformanceOverlay": "パフォーマンスオーバーレイを自動非表示",
"autoHidePerformanceOverlayDescription": "再生コントロールと一緒にパフォーマンスオーバーレイをフェードする",
"showNavBarLabels": "ナビゲーションバーラベルを表示",
"showNavBarLabelsDescription": "ナビゲーションバーアイコンの下にテキストラベルを表示",
"liveTvDefaultFavorites": "お気に入りチャンネルをデフォルトに",
"liveTvDefaultFavoritesDescription": "ライブTV を開いたときにお気に入りチャンネルのみ表示",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "映画、番組、音楽を検索...",
"tryDifferentTerm": "別の検索語をお試しください",
"searchYourMedia": "メディアを検索",
"enterTitleActorOrKeyword": "タイトル、俳優、またはキーワードを入力"
},
"hotkeys": {
"setShortcutFor": "${actionName}のショートカットを設定",
"clearShortcut": "ショートカットをクリア",
"actions": {
"playPause": "再生/一時停止",
"volumeUp": "音量を上げる",
"volumeDown": "音量を下げる",
"seekForward": "前方にシーク (${seconds}秒)",
"seekBackward": "後方にシーク (${seconds}秒)",
"fullscreenToggle": "フルスクリーン切替",
"muteToggle": "ミュート切替",
"subtitleToggle": "字幕切替",
"audioTrackNext": "次の音声トラック",
"subtitleTrackNext": "次の字幕トラック",
"chapterNext": "次のチャプター",
"chapterPrevious": "前のチャプター",
"speedIncrease": "速度を上げる",
"speedDecrease": "速度を下げる",
"speedReset": "速度をリセット",
"subSeekNext": "次の字幕にシーク",
"subSeekPrev": "前の字幕にシーク",
"shaderToggle": "シェーダー切替",
"skipMarker": "イントロ/クレジットをスキップ"
}
},
"fileInfo": {
"title": "ファイル情報",
"video": "映像",
"audio": "音声",
"file": "ファイル",
"advanced": "詳細",
"codec": "コーデック",
"resolution": "解像度",
"bitrate": "ビットレート",
"frameRate": "フレームレート",
"aspectRatio": "アスペクト比",
"profile": "プロファイル",
"bitDepth": "ビット深度",
"colorSpace": "色空間",
"colorRange": "色範囲",
"colorPrimaries": "色原色",
"chromaSubsampling": "クロマサブサンプリング",
"channels": "チャンネル",
"subtitles": "字幕",
"overallBitrate": "全体ビットレート",
"path": "パス",
"size": "サイズ",
"container": "コンテナ",
"duration": "長さ",
"optimizedForStreaming": "ストリーミング最適化",
"has64bitOffsets": "64ビットオフセット"
},
"mediaMenu": {
"markAsWatched": "視聴済みにする",
"markAsUnwatched": "未視聴にする",
"removeFromContinueWatching": "視聴中から削除",
"goToSeries": "シリーズへ移動",
"goToSeason": "シーズンへ移動",
"shufflePlay": "シャッフル再生",
"fileInfo": "ファイル情報",
"deleteFromServer": "サーバーから削除",
"confirmDelete": "このメディアとそのファイルがサーバーから完全に削除されます。この操作は元に戻せません。",
"deleteMultipleWarning": "すべてのエピソードとそのファイルが含まれます。",
"mediaDeletedSuccessfully": "メディアアイテムを正常に削除しました",
"mediaFailedToDelete": "メディアアイテムの削除に失敗しました",
"rate": "評価",
"playFromBeginning": "最初から再生",
"playVersion": "バージョンを再生..."
},
"accessibility": {
"mediaCardMovie": "${title}、映画",
"mediaCardShow": "${title}、テレビ番組",
"mediaCardEpisode": "${title}、${episodeInfo}",
"mediaCardSeason": "${title}、${seasonInfo}",
"mediaCardWatched": "視聴済み",
"mediaCardPartiallyWatched": "${percent}パーセント視聴済み",
"mediaCardUnwatched": "未視聴",
"tapToPlay": "タップして再生"
},
"tooltips": {
"shufflePlay": "シャッフル再生",
"playTrailer": "予告編を再生",
"markAsWatched": "視聴済みにする",
"markAsUnwatched": "未視聴にする"
},
"videoControls": {
"audioLabel": "音声",
"subtitlesLabel": "字幕",
"resetToZero": "0msにリセット",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label}遅く再生",
"playsEarlier": "${label}早く再生",
"noOffset": "オフセットなし",
"letterbox": "レターボックス",
"fillScreen": "画面を埋める",
"stretch": "引き延ばす",
"lockRotation": "回転をロック",
"unlockRotation": "回転のロックを解除",
"timerActive": "タイマー動作中",
"playbackWillPauseIn": "再生は${duration}後に一時停止します",
"stillWatching": "まだ視聴中ですか?",
"pausingIn": "${seconds}秒後に一時停止",
"continueWatching": "続ける",
"autoPlayNext": "次を自動再生",
"playNext": "次を再生",
"playButton": "再生",
"pauseButton": "一時停止",
"seekBackwardButton": "${seconds}秒戻る",
"seekForwardButton": "${seconds}秒進む",
"previousButton": "前のエピソード",
"nextButton": "次のエピソード",
"previousChapterButton": "前のチャプター",
"nextChapterButton": "次のチャプター",
"muteButton": "ミュート",
"unmuteButton": "ミュート解除",
"settingsButton": "動画設定",
"tracksButton": "音声と字幕",
"chaptersButton": "チャプター",
"versionsButton": "動画バージョン",
"pipButton": "ピクチャーインピクチャーモード",
"aspectRatioButton": "アスペクト比",
"ambientLighting": "アンビエントライティング",
"fullscreenButton": "フルスクリーンに入る",
"exitFullscreenButton": "フルスクリーンを終了",
"alwaysOnTopButton": "常に前面に表示",
"rotationLockButton": "回転ロック",
"lockScreen": "画面をロック",
"screenLockButton": "画面ロック",
"longPressToUnlock": "長押しでロック解除",
"timelineSlider": "動画タイムライン",
"volumeSlider": "音量レベル",
"endsAt": "${time}に終了",
"pipActive": "ピクチャーインピクチャーで再生中",
"pipFailed": "ピクチャーインピクチャーの開始に失敗しました",
"pipErrors": {
"androidVersion": "Android 8.0以降が必要です",
"iosVersion": "iOS 15.0以降が必要です",
"permissionDisabled": "ピクチャーインピクチャーの権限が無効です。設定 > アプリ > Plezy > ピクチャーインピクチャーで有効にしてください",
"notSupported": "デバイスはピクチャーインピクチャーモードをサポートしていません",
"voSwitchFailed": "ピクチャーインピクチャーの映像出力切替に失敗しました",
"failed": "ピクチャーインピクチャーの開始に失敗しました",
"unknown": "エラーが発生しました: ${error}"
},
"chapters": "チャプター",
"noChaptersAvailable": "チャプターがありません",
"queue": "キュー",
"noQueueItems": "キューにアイテムがありません",
"searchSubtitles": "字幕を検索",
"language": "言語",
"noSubtitlesFound": "字幕が見つかりません",
"subtitleDownloaded": "字幕をダウンロードしました",
"subtitleDownloadFailed": "字幕のダウンロードに失敗しました",
"searchLanguages": "言語を検索..."
},
"userStatus": {
"admin": "管理者",
"restricted": "制限付き",
"protected": "保護済み",
"current": "現在"
},
"messages": {
"markedAsWatched": "視聴済みにしました",
"markedAsUnwatched": "未視聴にしました",
"markedAsWatchedOffline": "視聴済みにしました(オンライン時に同期)",
"markedAsUnwatchedOffline": "未視聴にしました(オンライン時に同期)",
"autoRemovedWatchedDownload": "自動削除: ${title}",
"removedFromContinueWatching": "視聴中から削除しました",
"errorLoading": "エラー: ${error}",
"fileInfoNotAvailable": "ファイル情報が利用できません",
"errorLoadingFileInfo": "ファイル情報の読み込みエラー: ${error}",
"errorLoadingSeries": "シリーズの読み込みエラー",
"errorLoadingSeason": "シーズンの読み込みエラー",
"musicNotSupported": "音楽の再生はまだサポートされていません",
"logsCleared": "ログをクリアしました",
"logsCopied": "ログをクリップボードにコピーしました",
"noLogsAvailable": "ログがありません",
"libraryScanning": "\"${title}\"をスキャン中...",
"libraryScanStarted": "\"${title}\"のライブラリスキャンを開始しました",
"libraryScanFailed": "ライブラリのスキャンに失敗しました: ${error}",
"metadataRefreshing": "\"${title}\"のメタデータを更新中...",
"metadataRefreshStarted": "\"${title}\"のメタデータ更新を開始しました",
"metadataRefreshFailed": "メタデータの更新に失敗しました: ${error}",
"logoutConfirm": "ログアウトしてもよろしいですか?",
"noSeasonsFound": "シーズンが見つかりません",
"noEpisodesFound": "最初のシーズンにエピソードが見つかりません",
"noEpisodesFoundGeneral": "エピソードが見つかりません",
"noResultsFound": "結果が見つかりません",
"sleepTimerSet": "スリープタイマーを${label}に設定しました",
"noItemsAvailable": "アイテムがありません",
"failedToCreatePlayQueueNoItems": "再生キューの作成に失敗しました - アイテムがありません",
"failedPlayback": "${action}に失敗しました: ${error}",
"switchingToCompatiblePlayer": "互換プレーヤーに切替中...",
"serverLimitTitle": "再生に失敗しました",
"serverLimitBody": "サーバーからエラーが返されました (HTTP 500)。通常、これはサーバーの所有者が帯域幅またはトランスコードの制限を設定しており、セッションが拒否されていることを意味します。クライアント側でできることはありません — サーバーの所有者が設定を調整する必要があります。",
"logsUploaded": "ログをアップロードしました",
"logsUploadFailed": "ログのアップロードに失敗しました",
"logId": "ログID"
},
"subtitlingStyling": {
"text": "テキスト",
"border": "枠線",
"background": "背景",
"fontSize": "フォントサイズ",
"textColor": "テキストの色",
"borderSize": "枠線サイズ",
"borderColor": "枠線の色",
"backgroundOpacity": "背景の不透明度",
"backgroundColor": "背景色",
"position": "位置",
"assOverride": "ASSオーバーライド",
"bold": "太字",
"italic": "斜体"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "高度な動画プレーヤー設定",
"presets": "プリセット",
"noPresets": "保存済みプリセットがありません",
"saveAsPreset": "プリセットとして保存...",
"presetName": "プリセット名",
"presetNameHint": "プリセットの名前を入力",
"loadPreset": "読み込み",
"deletePreset": "削除",
"presetSaved": "プリセットを保存しました",
"presetLoaded": "プリセットを読み込みました",
"presetDeleted": "プリセットを削除しました",
"confirmDeletePreset": "このプリセットを削除してもよろしいですか?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "操作の確認"
},
"discover": {
"title": "探す",
"switchProfile": "プロフィール切替",
"noContentAvailable": "コンテンツがありません",
"addMediaToLibraries": "ライブラリにメディアを追加してください",
"continueWatching": "視聴を続ける",
"playEpisode": "S${season}E${episode}",
"overview": "あらすじ",
"cast": "キャスト",
"extras": "予告編とエクストラ",
"studio": "スタジオ",
"rating": "評価",
"movie": "映画",
"tvShow": "テレビ番組",
"minutesLeft": "残り${minutes}分"
},
"errors": {
"searchFailed": "検索に失敗しました: ${error}",
"connectionTimeout": "${context}の読み込み中に接続がタイムアウトしました",
"connectionFailed": "Plexサーバーに接続できません",
"failedToLoad": "${context}の読み込みに失敗しました: ${error}",
"noClientAvailable": "クライアントが利用できません",
"authenticationFailed": "認証に失敗しました: ${error}",
"couldNotLaunchUrl": "認証URLを開けませんでした",
"pleaseEnterToken": "トークンを入力してください",
"invalidToken": "無効なトークン",
"failedToVerifyToken": "トークンの検証に失敗しました: ${error}",
"failedToSwitchProfile": "${displayName}への切替に失敗しました"
},
"libraries": {
"title": "ライブラリ",
"scanLibraryFiles": "ライブラリファイルをスキャン",
"scanLibrary": "ライブラリをスキャン",
"analyze": "解析",
"analyzeLibrary": "ライブラリを解析",
"refreshMetadata": "メタデータを更新",
"emptyTrash": "ゴミ箱を空にする",
"emptyingTrash": "\"${title}\"のゴミ箱を空にしています...",
"trashEmptied": "\"${title}\"のゴミ箱を空にしました",
"failedToEmptyTrash": "ゴミ箱を空にできませんでした: ${error}",
"analyzing": "\"${title}\"を解析中...",
"analysisStarted": "\"${title}\"の解析を開始しました",
"failedToAnalyze": "ライブラリの解析に失敗しました: ${error}",
"noLibrariesFound": "ライブラリが見つかりません",
"thisLibraryIsEmpty": "このライブラリは空です",
"all": "すべて",
"clearAll": "すべてクリア",
"scanLibraryConfirm": "\"${title}\"をスキャンしてもよろしいですか?",
"analyzeLibraryConfirm": "\"${title}\"を解析してもよろしいですか?",
"refreshMetadataConfirm": "\"${title}\"のメタデータを更新してもよろしいですか?",
"emptyTrashConfirm": "\"${title}\"のゴミ箱を空にしてもよろしいですか?",
"manageLibraries": "ライブラリを管理",
"sort": "並べ替え",
"sortBy": "並べ替え順",
"filters": "フィルター",
"confirmActionMessage": "この操作を実行してもよろしいですか?",
"showLibrary": "ライブラリを表示",
"hideLibrary": "ライブラリを非表示",
"libraryOptions": "ライブラリオプション",
"content": "ライブラリコンテンツ",
"selectLibrary": "ライブラリを選択",
"filtersWithCount": "フィルター (${count})",
"noRecommendations": "おすすめがありません",
"noCollections": "このライブラリにコレクションがありません",
"noFoldersFound": "フォルダが見つかりません",
"folders": "フォルダ",
"tabs": {
"recommended": "おすすめ",
"browse": "ブラウズ",
"collections": "コレクション",
"playlists": "プレイリスト"
},
"groupings": {
"title": "グループ",
"all": "すべて",
"movies": "映画",
"shows": "テレビ番組",
"seasons": "シーズン",
"episodes": "エピソード",
"folders": "フォルダ"
}
},
"about": {
"title": "アプリについて",
"openSourceLicenses": "オープンソースライセンス",
"versionLabel": "バージョン ${version}",
"appDescription": "Flutter製の美しいPlexクライアント",
"viewLicensesDescription": "サードパーティライブラリのライセンスを表示"
},
"serverSelection": {
"allServerConnectionsFailed": "どのサーバーにも接続できませんでした。ネットワークを確認してもう一度お試しください。",
"noServersFoundForAccount": "${username} (${email})のサーバーが見つかりません",
"failedToLoadServers": "サーバーの読み込みに失敗しました: ${error}"
},
"hubDetail": {
"title": "タイトル",
"releaseYear": "公開年",
"dateAdded": "追加日",
"rating": "評価",
"noItemsFound": "アイテムが見つかりません"
},
"logs": {
"clearLogs": "ログをクリア",
"copyLogs": "ログをコピー",
"uploadLogs": "ログをアップロード"
},
"licenses": {
"relatedPackages": "関連パッケージ",
"license": "ライセンス",
"licenseNumber": "ライセンス ${number}",
"licensesCount": "${count}件のライセンス"
},
"navigation": {
"libraries": "ライブラリ",
"downloads": "ダウンロード",
"liveTv": "ライブTV"
},
"liveTv": {
"title": "ライブTV",
"guide": "番組表",
"noChannels": "チャンネルがありません",
"noDvr": "どのサーバーにもDVRが設定されていません",
"noPrograms": "番組データがありません",
"live": "ライブ",
"reloadGuide": "番組表を再読込",
"now": "現在",
"today": "今日",
"midnight": "深夜",
"overnight": "深夜",
"morning": "朝",
"daytime": "昼",
"evening": "夕方",
"lateNight": "深夜",
"whatsOn": "放送中",
"watchChannel": "チャンネルを視聴",
"favorites": "お気に入り",
"reorderFavorites": "お気に入りを並べ替え",
"joinSession": "進行中のセッションに参加",
"watchFromStart": "最初から視聴(${minutes}分前に開始)",
"watchLive": "ライブで視聴",
"goToLive": "ライブに移動"
},
"collections": {
"title": "コレクション",
"collection": "コレクション",
"empty": "コレクションは空です",
"unknownLibrarySection": "削除できません:不明なライブラリセクション",
"deleteCollection": "コレクションを削除",
"deleteConfirm": "\"${title}\"を削除してもよろしいですか?この操作は元に戻せません。",
"deleted": "コレクションを削除しました",
"deleteFailed": "コレクションの削除に失敗しました",
"deleteFailedWithError": "コレクションの削除に失敗しました: ${error}",
"failedToLoadItems": "コレクションアイテムの読み込みに失敗しました: ${error}",
"selectCollection": "コレクションを選択",
"collectionName": "コレクション名",
"enterCollectionName": "コレクション名を入力",
"addedToCollection": "コレクションに追加しました",
"errorAddingToCollection": "コレクションへの追加に失敗しました",
"created": "コレクションを作成しました",
"removeFromCollection": "コレクションから削除",
"removeFromCollectionConfirm": "\"${title}\"をこのコレクションから削除しますか?",
"removedFromCollection": "コレクションから削除しました",
"removeFromCollectionFailed": "コレクションからの削除に失敗しました",
"removeFromCollectionError": "コレクションからの削除エラー: ${error}",
"searchCollections": "コレクションを検索..."
},
"playlists": {
"title": "プレイリスト",
"playlist": "プレイリスト",
"noPlaylists": "プレイリストが見つかりません",
"create": "プレイリストを作成",
"playlistName": "プレイリスト名",
"enterPlaylistName": "プレイリスト名を入力",
"delete": "プレイリストを削除",
"removeItem": "プレイリストから削除",
"smartPlaylist": "スマートプレイリスト",
"itemCount": "${count}アイテム",
"oneItem": "1アイテム",
"emptyPlaylist": "このプレイリストは空です",
"deleteConfirm": "プレイリストを削除しますか?",
"deleteMessage": "\"${name}\"を削除してもよろしいですか?",
"created": "プレイリストを作成しました",
"deleted": "プレイリストを削除しました",
"itemAdded": "プレイリストに追加しました",
"itemRemoved": "プレイリストから削除しました",
"selectPlaylist": "プレイリストを選択",
"errorCreating": "プレイリストの作成に失敗しました",
"errorDeleting": "プレイリストの削除に失敗しました",
"errorLoading": "プレイリストの読み込みに失敗しました",
"errorAdding": "プレイリストへの追加に失敗しました",
"errorReordering": "プレイリストアイテムの並べ替えに失敗しました",
"errorRemoving": "プレイリストからの削除に失敗しました"
},
"watchTogether": {
"title": "一緒に見る",
"description": "友達や家族と同期してコンテンツを視聴",
"createSession": "セッションを作成",
"creating": "作成中...",
"joinSession": "セッションに参加",
"joining": "参加中...",
"controlMode": "コントロールモード",
"controlModeQuestion": "誰が再生を制御できますか?",
"hostOnly": "ホストのみ",
"anyone": "全員",
"hostingSession": "セッションをホスト中",
"inSession": "セッション中",
"sessionCode": "セッションコード",
"hostControlsPlayback": "ホストが再生を制御",
"anyoneCanControl": "全員が再生を制御可能",
"hostControls": "ホストが制御",
"anyoneControls": "全員が制御",
"participants": "参加者",
"host": "ホスト",
"hostBadge": "HOST",
"youAreHost": "あなたはホストです",
"watchingWithOthers": "他の人と視聴中",
"endSession": "セッションを終了",
"leaveSession": "セッションを退出",
"endSessionQuestion": "セッションを終了しますか?",
"leaveSessionQuestion": "セッションを退出しますか?",
"endSessionConfirm": "すべての参加者のセッションが終了します。",
"leaveSessionConfirm": "セッションから退出されます。",
"endSessionConfirmOverlay": "すべての参加者の視聴セッションが終了します。",
"leaveSessionConfirmOverlay": "視聴セッションから切断されます。",
"end": "終了",
"leave": "退出",
"syncing": "同期中...",
"joinWatchSession": "視聴セッションに参加",
"enterCodeHint": "5文字のコードを入力",
"pasteFromClipboard": "クリップボードから貼り付け",
"pleaseEnterCode": "セッションコードを入力してください",
"codeMustBe5Chars": "セッションコードは5文字である必要があります",
"joinInstructions": "ホストが共有したセッションコードを入力して視聴セッションに参加してください。",
"failedToCreate": "セッションの作成に失敗しました",
"failedToJoin": "セッションへの参加に失敗しました",
"sessionCodeCopied": "セッションコードをクリップボードにコピーしました",
"relayUnreachable": "リレーサーバーに到達できません。ISPが接続をブロックしている可能性があります。試すことはできますが、一緒に見る機能が動作しない場合があります。",
"reconnectingToHost": "ホストに再接続中...",
"currentPlayback": "現在の再生",
"joinCurrentPlayback": "現在の再生に参加",
"joinCurrentPlaybackDescription": "ホストが現在視聴中のコンテンツに戻る",
"failedToOpenCurrentPlayback": "現在の再生を開けませんでした",
"participantJoined": "${name}が参加しました",
"participantLeft": "${name}が退出しました",
"participantPaused": "${name}が一時停止しました",
"participantResumed": "${name}が再開しました",
"participantSeeked": "${name}がシークしました",
"participantBuffering": "${name}がバッファリング中",
"waitingForParticipants": "他の参加者の読み込みを待っています...",
"recentRooms": "最近のルーム",
"renameRoom": "ルーム名を変更",
"removeRoom": "削除"
},
"downloads": {
"title": "ダウンロード",
"manage": "管理",
"tvShows": "テレビ番組",
"movies": "映画",
"noDownloads": "ダウンロードなし",
"noDownloadsDescription": "ダウンロードしたコンテンツはここに表示され、オフラインで視聴できます",
"downloadNow": "ダウンロード",
"deleteDownload": "ダウンロードを削除",
"retryDownload": "ダウンロードを再試行",
"downloadQueued": "ダウンロードをキューに追加しました",
"serverErrorBitrate": "サーバーエラー — ファイルがリモートストリーミングのビットレート制限を超えている可能性があります",
"episodesQueued": "${count}エピソードをダウンロードキューに追加しました",
"downloadDeleted": "ダウンロードを削除しました",
"deleteConfirm": "\"${title}\"を削除してもよろしいですか?ダウンロードしたファイルがデバイスから削除されます。",
"deletingWithProgress": "${title}を削除中... (${current}/${total})",
"noDownloadsTree": "ダウンロードなし",
"pauseAll": "すべて一時停止",
"resumeAll": "すべて再開",
"deleteAll": "すべて削除",
"selectVersion": "バージョンを選択",
"allEpisodes": "すべてのエピソード",
"unwatchedOnly": "未視聴のみ",
"nextNUnwatched": "次の${count}件の未視聴",
"customAmount": "数を指定...",
"howManyEpisodes": "何エピソード?",
"itemsQueued": "${count}件をダウンロードキューに追加",
"keepSynced": "同期を維持",
"downloadOnce": "一度だけダウンロード",
"keepNUnwatched": "未視聴を${count}件保持",
"editSyncRule": "同期ルールを編集",
"removeSyncRule": "同期ルールを削除",
"removeSyncRuleConfirm": "「${title}」の同期を停止しますか?ダウンロード済みのエピソードは保持されます。",
"syncRuleCreated": "同期ルールを作成しました — 未視聴のエピソードを${count}件保持",
"syncRuleUpdated": "同期ルールを更新しました",
"syncRuleRemoved": "同期ルールを削除しました",
"syncedNewEpisodes": "${title}の新しいエピソードを${count}件同期しました",
"activeSyncRules": "同期ルール",
"noSyncRules": "同期ルールなし",
"manageSyncRule": "同期を管理",
"editEpisodeCount": "エピソード数",
"editSyncFilter": "同期フィルター",
"syncAllItems": "すべてのアイテムを同期中",
"syncUnwatchedItems": "未視聴のアイテムを同期中",
"syncRuleListCreated": "同期ルールを作成しました"
},
"shaders": {
"title": "シェーダー",
"noShaderDescription": "映像補正なし",
"nvscalerDescription": "よりシャープな映像のためのNVIDIA画像スケーリング",
"qualityFast": "高速",
"qualityHQ": "高品質",
"mode": "モード",
"importShader": "シェーダーをインポート",
"customShaderDescription": "カスタムGLSLシェーダー",
"shaderImported": "シェーダーをインポートしました",
"shaderImportFailed": "シェーダーのインポートに失敗しました",
"deleteShader": "シェーダーを削除",
"deleteShaderConfirm": "\"${name}\"を削除しますか?"
},
"companionRemote": {
"title": "コンパニオンリモート",
"connectedTo": "${name}に接続中",
"session": {
"startingServer": "リモートサーバーを起動中...",
"failedToCreate": "リモートサーバーの起動に失敗しました:",
"hostAddress": "ホストアドレス",
"connected": "接続済み",
"serverRunning": "リモートサーバー稼働中",
"serverStopped": "リモートサーバー停止中",
"serverRunningDescription": "ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます",
"serverStoppedDescription": "モバイルデバイスの接続を許可するにはサーバーを起動してください",
"usePhoneToControl": "モバイルデバイスでこのアプリを操作できます",
"startServer": "サーバーを起動",
"stopServer": "サーバーを停止",
"minimize": "最小化"
},
"pairing": {
"discoveryDescription": "同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "接続中...",
"searchingForDevices": "デバイスを検索中...",
"noDevicesFound": "ネットワーク上にデバイスが見つかりません",
"noDevicesHint": "デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください",
"availableDevices": "利用可能なデバイス",
"manualConnection": "手動接続",
"cryptoInitFailed": "安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。",
"validationHostRequired": "ホストアドレスを入力してください",
"validationHostFormat": "形式はIP:ポートである必要があります(例: 192.168.1.100:48632",
"connectionTimedOut": "接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください。",
"sessionNotFound": "デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。",
"authFailed": "認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください。",
"failedToConnect": "接続に失敗しました: ${error}"
},
"remote": {
"disconnectConfirm": "リモートセッションから切断しますか?",
"reconnecting": "再接続中...",
"attemptOf": "試行 ${current}/5",
"retryNow": "今すぐ再試行",
"tabRemote": "リモート",
"tabPlay": "再生",
"tabMore": "その他",
"menu": "メニュー",
"tabNavigation": "タブナビゲーション",
"tabDiscover": "探す",
"tabLibraries": "ライブラリ",
"tabSearch": "検索",
"tabDownloads": "ダウンロード",
"tabSettings": "設定",
"previous": "前へ",
"playPause": "再生/一時停止",
"next": "次へ",
"seekBack": "巻き戻し",
"stop": "停止",
"seekForward": "早送り",
"volume": "音量",
"volumeDown": "下げる",
"volumeUp": "上げる",
"fullscreen": "フルスクリーン",
"subtitles": "字幕",
"audio": "音声",
"searchHint": "デスクトップで検索..."
}
},
"videoSettings": {
"playbackSettings": "再生設定",
"playbackSpeed": "再生速度",
"sleepTimer": "スリープタイマー",
"audioSync": "音声同期",
"subtitleSync": "字幕同期",
"hdr": "HDR",
"audioOutput": "音声出力",
"performanceOverlay": "パフォーマンスオーバーレイ",
"audioPassthrough": "オーディオパススルー",
"audioNormalization": "音声正規化"
},
"externalPlayer": {
"title": "外部プレーヤー",
"useExternalPlayer": "外部プレーヤーを使用",
"useExternalPlayerDescription": "内蔵プレーヤーの代わりに外部アプリで動画を開く",
"selectPlayer": "プレーヤーを選択",
"customPlayers": "カスタムプレーヤー",
"systemDefault": "システムデフォルト",
"addCustomPlayer": "カスタムプレーヤーを追加",
"playerName": "プレーヤー名",
"playerCommand": "コマンド",
"playerPackage": "パッケージ名",
"playerUrlScheme": "URLスキーム",
"off": "オフ",
"launchFailed": "外部プレーヤーの起動に失敗しました",
"appNotInstalled": "${name}がインストールされていません",
"playInExternalPlayer": "外部プレーヤーで再生"
},
"metadataEdit": {
"editMetadata": "編集...",
"screenTitle": "メタデータを編集",
"basicInfo": "基本情報",
"artwork": "アートワーク",
"advancedSettings": "詳細設定",
"title": "タイトル",
"sortTitle": "ソートタイトル",
"originalTitle": "原題",
"releaseDate": "公開日",
"contentRating": "コンテンツレーティング",
"studio": "スタジオ",
"tagline": "タグライン",
"summary": "あらすじ",
"poster": "ポスター",
"background": "背景",
"logo": "ロゴ",
"squareArt": "正方形アート",
"selectPoster": "ポスターを選択",
"selectBackground": "背景を選択",
"selectLogo": "ロゴを選択",
"selectSquareArt": "正方形アートを選択",
"fromUrl": "URLから",
"uploadFile": "ファイルをアップロード",
"enterImageUrl": "画像URLを入力",
"imageUrl": "画像URL",
"metadataUpdated": "メタデータを更新しました",
"metadataUpdateFailed": "メタデータの更新に失敗しました",
"artworkUpdated": "アートワークを更新しました",
"artworkUpdateFailed": "アートワークの更新に失敗しました",
"noArtworkAvailable": "アートワークがありません",
"notSet": "未設定",
"libraryDefault": "ライブラリのデフォルト",
"accountDefault": "アカウントのデフォルト",
"seriesDefault": "シリーズのデフォルト",
"episodeSorting": "エピソードの並べ替え",
"oldestFirst": "古い順",
"newestFirst": "新しい順",
"keep": "保持",
"allEpisodes": "すべてのエピソード",
"latestEpisodes": "最新${count}エピソード",
"latestEpisode": "最新エピソード",
"episodesAddedPastDays": "過去${count}日間に追加されたエピソード",
"deleteAfterPlaying": "再生後にエピソードを削除",
"never": "しない",
"afterADay": "1日後",
"afterAWeek": "1週間後",
"afterAMonth": "1ヶ月後",
"onNextRefresh": "次回更新時",
"seasons": "シーズン",
"show": "表示",
"hide": "非表示",
"episodeOrdering": "エピソードの順序",
"tmdbAiring": "The Movie Database(放送順)",
"tvdbAiring": "TheTVDB(放送順)",
"tvdbAbsolute": "TheTVDB(絶対順)",
"metadataLanguage": "メタデータの言語",
"useOriginalTitle": "原題を使用",
"preferredAudioLanguage": "優先音声言語",
"preferredSubtitleLanguage": "優先字幕言語",
"subtitleMode": "字幕自動選択モード",
"manuallySelected": "手動選択",
"shownWithForeignAudio": "外国語音声時に表示",
"alwaysEnabled": "常に有効",
"tags": "タグ",
"addTag": "タグを追加",
"genre": "ジャンル",
"director": "監督",
"writer": "脚本",
"producer": "プロデューサー",
"country": "国",
"collection": "コレクション",
"label": "ラベル",
"style": "スタイル",
"mood": "ムード"
},
"serverTasks": {
"title": "サーバータスク",
"failedToLoad": "タスクの読み込みに失敗しました",
"noTasks": "実行中のタスクはありません"
},
"trakt": {
"title": "Trakt",
"connected": "接続済み",
"connectedAs": "@${username} として接続済み",
"disconnectConfirm": "Trakt アカウントを切断しますか?",
"disconnectConfirmBody": "Plezy から Trakt への再生イベント送信が停止します。いつでも再接続できます。",
"scrobble": "リアルタイムのスクロブル",
"scrobbleDescription": "再生中に再生・一時停止・停止イベントを Trakt に送信します。",
"watchedSync": "視聴済みステータスを同期",
"watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。",
"deviceCodeTitle": "Trakt で Plezy をアクティベート",
"deviceCodeBody": "${url} を開き、このコードを入力してください:",
"openTraktActivate": "アクティベートするには Trakt を開く",
"waitingForAuthorization": "認証を待機中…",
"codeCopied": "コードをコピーしました",
"connectFailed": "Trakt に接続できませんでした。もう一度お試しください。"
}
}
+204 -121
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Plex 계정으로 로그인",
"showQRCode": "QR 코드",
"authenticate": "인증",
"debugEnterToken": "디버깅을 위해 Plex 토큰을 입력하세요.",
"plexTokenLabel": "Plex 인증 토큰",
"plexTokenHint": "Plex.tv 토큰을 입력하세요",
"authenticationTimeout": "인증 시간이 초과되었습니다. 다시 시도해 주세요.",
"scanQRToSignIn": "QR 코드를 스캔하여 로그인",
"waitingForAuth": "인증 대기 중... 브라우저에서 로그인을 완료해 주세요.",
@@ -32,8 +29,7 @@
"delete": "삭제",
"shuffle": "무작위 재생",
"addTo": "추가하기...",
"remove": "삭제",
"paste": "붙여넣기",
"createNew": "새로 만들기",
"connect": "연결",
"disconnect": "연결 해제",
"play": "재생",
@@ -46,7 +42,6 @@
"settings": "설정",
"mute": "음소거",
"ok": "확인",
"loading": "로딩 중...",
"reconnect": "다시 연결",
"exitConfirmTitle": "앱을 종료하시겠습니까?",
"exitConfirmMessage": "정말 종료하시겠습니까?",
@@ -57,13 +52,14 @@
"refreshingServers": "서버 새로고침 중...",
"loadingServers": "서버 로딩 중...",
"connectingToServers": "서버 연결 중...",
"startingOfflineMode": "오프라인 모드 시작 중..."
"startingOfflineMode": "오프라인 모드 시작 중...",
"loading": "로딩 중..."
},
"screens": {
"licenses": "라이선스",
"switchProfile": "프로필 전환",
"subtitleStyling": "자막 스타일 설정",
"mpvConfig": "MPV 설정",
"mpvConfig": "mpv.conf",
"logs": "로그"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "설정",
"supportDeveloper": "Plezy 후원하기",
"supportDeveloperDescription": "Liberapay로 후원하여 개발 지원",
"language": "언어",
"theme": "테마",
"appearance": "외관",
"videoPlayback": "비디오 재생",
"videoPlaybackDescription": "재생 동작 구성",
"advanced": "고급",
"episodePosterMode": "에피소드 포스터 스타일",
"seriesPoster": "시리즈 포스터",
"seriesPosterDescription": "모든 에피소드에 시리즈 포스터 표시",
"seasonPoster": "시즌 포스터",
"seasonPosterDescription": "에피소드에 시즌별 포스터 표시",
"episodeThumbnail": "에피소드 썸네일",
"episodeThumbnailDescription": "16:9 에피소드 스크린샷 썸네일 표시",
"episodeThumbnail": "썸네일",
"showHeroSectionDescription": "홈 화면에 주요 콘텐츠 캐러셀(슬라이드) 표시",
"secondsLabel": "초",
"minutesLabel": "분",
@@ -96,23 +92,15 @@
"minutesShort": "분",
"durationHint": "기간 입력 (${min}-${max})",
"systemTheme": "시스템 설정",
"systemThemeDescription": "시스템 설정에 따름",
"lightTheme": "라이트 모드",
"darkTheme": "다크 모드",
"oledTheme": "OLED",
"oledThemeDescription": "OLED 화면을 위한 순수 검정",
"libraryDensity": "라이브러리 표시 밀도",
"compact": "좁게",
"compactDescription": "카드를 작게 표시하여 더 많은 항목을 보여줍니다.",
"normal": "보통",
"normalDescription": "기본 크기",
"comfortable": "넓게",
"comfortableDescription": "카드를 크게 표시하여 더 적은 항목을 보여줍니다.",
"viewMode": "보기 모드",
"gridView": "그리드 보기",
"gridViewDescription": "항목을 그리드 레이아웃으로 표시합니다",
"listView": "목록 보기",
"listViewDescription": "항목을 목록 레이아웃으로 표시합니다",
"showHeroSection": "주요 추천 영역 표시",
"useGlobalHubs": "Plex 홈 레이아웃 사용",
"useGlobalHubsDescription": "공식 Plex 클라이언트처럼 홈 페이지 허브를 표시합니다. 끄면 라이브러리별 추천이 대신 표시됩니다.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다",
"playerBackend": "플레이어 백엔드",
"exoPlayer": "ExoPlayer (권장)",
"exoPlayerDescription": "더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어",
"mpv": "MPV",
"mpvDescription": "더 많은 기능과 ASS 자막을 지원하는 고급 플레이어",
"mpv": "mpv",
"hardwareDecoding": "하드웨어 디코딩",
"hardwareDecodingDescription": "가능한 경우 하드웨어 가속을 사용합니다",
"bufferSize": "버퍼 크기",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "자막의 외형을 사용자 설정",
"smallSkipDuration": "짧은 건너뛰기 시간",
"largeSkipDuration": "긴 건너뛰기 시간",
"rewindOnResume": "재개 시 되감기",
"secondsUnit": "${seconds}초",
"defaultSleepTimer": "기본 취침 타이머",
"minutesUnit": "${minutes}분",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "사용자 정의 키보드 단축키",
"videoPlayerNavigation": "비디오 플레이어 탐색",
"videoPlayerNavigationDescription": "방향 키를 사용하여 비디오 플레이어 컨트롤 탐색",
"watchTogetherRelay": "함께 보기 릴레이",
"watchTogetherRelayDescription": "함께 보기에 사용할 사용자 지정 릴레이 서버를 설정합니다. 모든 참가자가 동일한 서버를 사용해야 합니다.",
"watchTogetherRelayHint": "https://my-relay.example.com",
"crashReporting": "충돌 보고",
"crashReportingDescription": "앱 개선을 위해 충돌 보고서 전송",
"debugLogging": "디버그 로깅",
"debugLoggingDescription": "문제 해결을 위해 상세 로깅 활성화",
"viewLogs": "로그 보기",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "엔딩 크레딧 자동 건너뛰기 후 다음 에피소드 재생",
"autoSkipDelay": "자동 건너뛰기 지연",
"autoSkipDelayDescription": "자동 건너뛰기 전 ${seconds} 초 대기",
"introPattern": "인트로 마커 패턴",
"introPatternDescription": "챕터 제목에서 인트로 마커를 인식하는 정규식 패턴",
"creditsPattern": "크레딧 마커 패턴",
"creditsPatternDescription": "챕터 제목에서 크레딧 마커를 인식하는 정규식 패턴",
"invalidRegex": "잘못된 정규식",
"downloads": "다운로드",
"downloadLocationDescription": "다운로드 콘텐츠 저장 위치 선택",
"downloadLocationDefault": "기본값 (앱 저장소)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "폴더 선택 실패",
"downloadOnWifiOnly": "WiFi 연결 시에만 다운로드",
"downloadOnWifiOnlyDescription": "셀룰러 데이터 사용 시 다운로드 불가",
"autoRemoveWatchedDownloads": "시청한 다운로드 자동 삭제",
"autoRemoveWatchedDownloadsDescription": "시청 완료로 표시된 에피소드와 영화의 다운로드를 자동으로 삭제",
"cellularDownloadBlocked": "셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요.",
"maxVolume": "최대 볼륨",
"maxVolumeDescription": "조용한 미디어를 위해 100% 이상의 볼륨 허용",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Discord에서 시청 중인 콘텐츠 표시",
"trakt": "Trakt",
"traktDescription": "Trakt와 시청 기록 동기화",
"companionRemoteServer": "컴패니언 리모트 서버",
"companionRemoteServerDescription": "네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용",
"autoPip": "자동 PIP 모드",
"autoPipDescription": "재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환",
"matchContentFrameRate": "콘텐츠 프레임 레이트 맞춤",
"matchContentFrameRateDescription": "비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "주사율 맞춤",
"matchRefreshRateDescription": "전체 화면에서 비디오 콘텐츠에 맞게 디스플레이 주사율을 전환합니다",
"matchDynamicRange": "다이나믹 레인지 맞춤",
"matchDynamicRangeDescription": "HDR 콘텐츠에 대해 자동으로 HDR을 활성화하고 플레이어를 나갈 때 SDR로 복원합니다",
"displaySwitchDelay": "디스플레이 전환 지연",
"tunneledPlayback": "터널 재생",
"tunneledPlaybackDescription": "하드웨어 가속 비디오 터널링을 사용합니다. HDR 콘텐츠에서 소리만 나고 검은 화면이 보이면 비활성화하세요",
"requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택",
"requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다",
"confirmExitOnBack": "종료 전 확인",
"confirmExitOnBackDescription": "뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다"
"confirmExitOnBackDescription": "뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다",
"forceTvMode": "TV 모드 강제 사용",
"forceTvModeDescription": "자동 감지와 관계없이 TV 레이아웃을 사용합니다. leanback 기능을 보고하지 않는 Android TV 기기에 유용합니다. 변경 시 앱이 다시 시작됩니다.",
"autoHidePerformanceOverlay": "성능 오버레이 자동 숨기기",
"autoHidePerformanceOverlayDescription": "재생 컨트롤과 함께 성능 오버레이를 페이드 처리",
"showNavBarLabels": "내비게이션 바 라벨 표시",
"showNavBarLabelsDescription": "내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다",
"liveTvDefaultFavorites": "즐겨찾기 채널 기본 설정",
"liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
@@ -239,11 +263,6 @@
"skipMarker": "인트로/크레딧 건너뛰기"
}
},
"pinEntry": {
"enterPin": "PIN 입력",
"showPin": "PIN 표시",
"hidePin": "PIN 숨기기"
},
"fileInfo": {
"title": "파일 정보",
"video": "비디오",
@@ -262,6 +281,8 @@
"colorPrimaries": "색상 원색",
"chromaSubsampling": "채도 서브샘플링",
"channels": "채널",
"subtitles": "자막",
"overallBitrate": "전체 비트레이트",
"path": "경로",
"size": "크기",
"container": "컨테이너",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "모든 에피소드와 파일이 포함됩니다.",
"mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다",
"mediaFailedToDelete": "미디어 항목 삭제 실패",
"rate": "평가"
"rate": "평가",
"playFromBeginning": "처음부터 재생",
"playVersion": "버전 재생..."
},
"accessibility": {
"mediaCardMovie": "${title}, 영화",
@@ -316,7 +339,6 @@
"unlockRotation": "회전 잠금 해제",
"timerActive": "타이머 활성화됨",
"playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다",
"sleepTimerCompleted": "수면 타이머 완료됨 - 재생이 일시 중지되었습니다",
"stillWatching": "아직 시청 중이신가요?",
"pausingIn": "${seconds}초 후 일시 정지",
"continueWatching": "계속",
@@ -333,34 +355,43 @@
"muteButton": "음소거",
"unmuteButton": "음소거 해제",
"settingsButton": "동영상 설정",
"audioTrackButton": "음원 트랙",
"subtitlesButton": "자막",
"tracksButton": "오디오 및 자막",
"chaptersButton": "챕터",
"versionsButton": "동영상 버전",
"pipButton": "픽처 인 픽처 모드",
"aspectRatioButton": "화면비율",
"ambientLighting": "주변 조명",
"ambientLightingOn": "주변 조명 켜기",
"ambientLightingOff": "주변 조명 끄기",
"fullscreenButton": "전체화면",
"exitFullscreenButton": "전체화면 종료",
"alwaysOnTopButton": "창 최상위 고정",
"rotationLockButton": "회전 잠금",
"lockScreen": "화면 잠금",
"screenLockButton": "화면 잠금",
"longPressToUnlock": "길게 눌러 잠금 해제",
"timelineSlider": "타임라인",
"volumeSlider": "볼륨 조절",
"endsAt": "${time}에 종료",
"pipActive": "화면 속 화면으로 재생 중",
"pipFailed": "화면 속 화면 모드를 시작할 수 없습니다",
"pipErrors": {
"androidVersion": "Android 8.0 이상이 필요합니다",
"iosVersion": "iOS 15.0 이상이 필요합니다",
"permissionDisabled": "화면 속 화면 권한이 비활성화되어 있습니다. 설정 > 앱 > Plezy > 화면 속 화면에서 활성화하세요",
"notSupported": "이 기기는 화면 속 화면 모드를 지원하지 않습니다",
"voSwitchFailed": "화면 속 화면을 위한 비디오 출력 전환에 실패했습니다",
"failed": "화면 속 화면 모드를 시작할 수 없습니다",
"unknown": "오류가 발생했습니다: ${error}"
},
"chapters": "챕터",
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "재생 대기열",
"noQueueItems": "대기열에 항목이 없습니다",
"searchSubtitles": "자막 검색",
"language": "언어",
"noSubtitlesFound": "자막을 찾을 수 없습니다",
"subtitleDownloaded": "자막이 다운로드되었습니다",
"subtitleDownloadFailed": "자막 다운로드에 실패했습니다",
"searchLanguages": "언어 검색..."
},
"userStatus": {
"admin": "관리자",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "시청 안 함으로 표시됨",
"markedAsWatchedOffline": "시청 완료로 표시됨 (연결 시 동기화됨)",
"markedAsUnwatchedOffline": "미시청으로 표시됨 (연결 시 동기화됨)",
"autoRemovedWatchedDownload": "자동 삭제됨: ${title}",
"removedFromContinueWatching": "계속 시청 목록에서 제거됨",
"errorLoading": "오류: ${error}",
"fileInfoNotAvailable": "파일 정보가 없습니다",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "재생 대기열 생성 실패 - 항목 없음",
"failedPlayback": "${action}을(를) 수행할 수 없습니다: ${error}",
"switchingToCompatiblePlayer": "호환되는 플레이어로 전환 중...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "재생 실패",
"serverLimitBody": "서버에서 오류를 반환했습니다 (HTTP 500). 일반적으로 이는 서버 소유자가 대역폭 또는 트랜스코딩 제한을 설정하여 세션을 거부하고 있다는 의미입니다. 클라이언트에서 할 수 있는 일은 없습니다 — 서버 소유자가 설정을 조정해야 합니다.",
"logsUploaded": "로그 업로드 완료",
"logsUploadFailed": "로그 업로드 실패",
"logId": "로그 ID"
},
"subtitlingStyling": {
"stylingOptions": "스타일 옵션",
"text": "텍스트",
"border": "테두리",
"background": "배경",
"fontSize": "글자 크기",
"textColor": "텍스트 색상",
"borderSize": "테두리 크기",
"borderColor": "테두리 색상",
"backgroundOpacity": "배경 불투명도",
"backgroundColor": "배경색",
"position": "Position"
"position": "위치",
"assOverride": "ASS 오버라이드",
"bold": "굵게",
"italic": "기울임꼴"
},
"mpvConfig": {
"title": "MPV 설정",
"title": "mpv 설정",
"description": "고급 비디오 플레이어 설정",
"properties": "속성",
"presets": "사전 설정",
"noProperties": "설정된 속성이 없습니다",
"noPresets": "저장된 사전 설정이 없습니다",
"addProperty": "속성 추가",
"editProperty": "속성 편집",
"deleteProperty": "속성 삭제",
"propertyKey": "속성 키",
"propertyKeyHint": "예: hwdec, demuxer-max-bytes",
"propertyValue": "속성값",
"propertyValueHint": "예: auto, 256000000",
"saveAsPreset": "프리셋으로 저장...",
"presetName": "프리셋 이름",
"presetNameHint": "이 프리셋의 이름을 입력하세요",
@@ -436,8 +466,7 @@
"presetLoaded": "프리셋이 로드 되었습니다",
"presetDeleted": "프리셋이 삭제 되었습니다",
"confirmDeletePreset": "이 프리셋을 삭제 하시겠습니까?",
"confirmDeleteProperty": "이 속성을 삭제 하시겠습니까?",
"entriesCount": "${count} 항목"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "확인"
@@ -452,11 +481,8 @@
"overview": "개요",
"cast": "출연진",
"extras": "예고편 및 부가영상",
"seasons": "시즌 수",
"studio": "제작사",
"rating": "연령 등급",
"episodeCount": "${count} 편",
"watchedProgress": "${watched}/${total} 편 시청 완료",
"movie": "영화",
"tvShow": "TV 시리즈",
"minutesLeft": "${minutes}분 남음"
@@ -518,6 +544,7 @@
"playlists": "재생 목록"
},
"groupings": {
"title": "그룹",
"all": "전체",
"movies": "영화",
"shows": "TV 프로그램",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "로그 지우기",
"copyLogs": "로그 복사",
"uploadLogs": "Upload Logs",
"error": "오류:",
"stackTrace": "스택 추적 (Stack Trace):"
"uploadLogs": "로그 업로드"
},
"licenses": {
"relatedPackages": "관련 소프트웨어 패키지",
@@ -565,20 +590,12 @@
},
"liveTv": {
"title": "실시간 TV",
"channels": "채널",
"guide": "편성표",
"noChannels": "사용 가능한 채널이 없습니다",
"noDvr": "서버에 DVR이 구성되어 있지 않습니다",
"tuneFailed": "채널 튜닝에 실패했습니다",
"loading": "채널 로딩 중...",
"nowPlaying": "현재 재생 중",
"noPrograms": "프로그램 데이터가 없습니다",
"channelNumber": "채널 ${number}",
"live": "실시간",
"hd": "HD",
"premiere": "신규",
"reloadGuide": "편성표 새로고침",
"allChannels": "전체 채널",
"now": "지금",
"today": "오늘",
"midnight": "자정",
@@ -588,7 +605,13 @@
"evening": "저녁",
"lateNight": "심야 방송",
"whatsOn": "지금 방송 중",
"watchChannel": "채널 시청"
"watchChannel": "채널 시청",
"favorites": "즐겨찾기",
"reorderFavorites": "즐겨찾기 순서 변경",
"joinSession": "진행 중인 세션 참여",
"watchFromStart": "처음부터 시청 (${minutes}분 전 시작)",
"watchLive": "실시간 시청",
"goToLive": "실시간으로 이동"
},
"collections": {
"title": "컬렉션",
@@ -602,7 +625,6 @@
"deleteFailedWithError": "컬렉션 삭제 실패: ${error}",
"failedToLoadItems": "컬렉션 항목 로드 실패: ${error}",
"selectCollection": "컬렉션 선택",
"createNewCollection": "새 컬렉션 생성",
"collectionName": "컬렉션 이름",
"enterCollectionName": "컬렉션 이름 입력",
"addedToCollection": "컬렉션에 추가됨",
@@ -612,7 +634,8 @@
"removeFromCollectionConfirm": "${title}을/를 이 컬렉션에서 제거 하시겠습니까?",
"removedFromCollection": "컬렉션에서 제거됨",
"removeFromCollectionFailed": "컬렉션에서 제거 실패",
"removeFromCollectionError": "컬렉션에서 제거 중 오류 발생: ${error}"
"removeFromCollectionError": "컬렉션에서 제거 중 오류 발생: ${error}",
"searchCollections": "컬렉션 검색..."
},
"playlists": {
"title": "플레이리스트",
@@ -634,7 +657,6 @@
"itemAdded": "재생 목록에 추가 되었습니다",
"itemRemoved": "재생 목록에서 제거됨",
"selectPlaylist": "재생 목록 선택",
"createNewPlaylist": "새 재생 목록 생성",
"errorCreating": "재생 목록 생성 실패",
"errorDeleting": "재생 목록 삭제 실패",
"errorLoading": "재생 목록 로드 실패",
@@ -677,18 +699,30 @@
"leave": "이탈",
"syncing": "동기화 중...",
"joinWatchSession": "시청 세션에 참여",
"enterCodeHint": "8자리 코드 입력",
"enterCodeHint": "5자리 코드 입력",
"pasteFromClipboard": "클립보드에서 붙여넣기",
"pleaseEnterCode": "세션 코드를 입력하세요",
"codeMustBe8Chars": "세션 코드는 반드시 8자리여야 합니다",
"codeMustBe5Chars": "세션 코드는 반드시 5자리여야 합니다",
"joinInstructions": "호스트가 공유한 세션 코드를 입력하여 시청 세션에 참여하세요.",
"failedToCreate": "세션 생성 실패",
"failedToJoin": "세션 참여 실패",
"sessionCodeCopied": "세션 코드가 클립보드에 복사되었습니다",
"relayUnreachable": "릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.",
"reconnectingToHost": "호스트에 재연결 중...",
"currentPlayback": "현재 재생",
"joinCurrentPlayback": "현재 재생 참여",
"joinCurrentPlaybackDescription": "호스트가 현재 보고 있는 항목으로 돌아갑니다",
"failedToOpenCurrentPlayback": "현재 재생을 열 수 없습니다",
"participantJoined": "${name}님이 참여했습니다",
"participantLeft": "${name}님이 나갔습니다"
"participantLeft": "${name}님이 나갔습니다",
"participantPaused": "${name}님이 일시정지했습니다",
"participantResumed": "${name}님이 재생했습니다",
"participantSeeked": "${name}님이 탐색했습니다",
"participantBuffering": "${name}님이 버퍼링 중입니다",
"waitingForParticipants": "다른 참가자의 로딩을 기다리는 중...",
"recentRooms": "최근 방",
"renameRoom": "방 이름 변경",
"removeRoom": "제거"
},
"downloads": {
"title": "다운로드",
@@ -701,6 +735,7 @@
"deleteDownload": "다운로드 삭제",
"retryDownload": "다운로드 재시도",
"downloadQueued": "다운로드 대기 중",
"serverErrorBitrate": "서버 오류 — 파일이 원격 스트리밍 비트레이트 제한을 초과할 수 있습니다",
"episodesQueued": "${count} 에피소드가 다운로드 대기열에 추가 되었습니다",
"downloadDeleted": "다운로드 삭제됨",
"deleteConfirm": "\"${title}\"를 삭제 하시겠습니까? 다운로드한 파일이 기기에서 삭제됩니다.",
@@ -708,7 +743,32 @@
"noDownloadsTree": "다운로드 없음",
"pauseAll": "모두 일시정지",
"resumeAll": "모두 재개",
"deleteAll": "모두 삭제"
"deleteAll": "모두 삭제",
"selectVersion": "버전 선택",
"allEpisodes": "모든 에피소드",
"unwatchedOnly": "시청하지 않은 것만",
"nextNUnwatched": "다음 ${count}개 미시청",
"customAmount": "직접 입력...",
"howManyEpisodes": "몇 개의 에피소드?",
"itemsQueued": "${count}개 항목이 다운로드 대기열에 추가됨",
"keepSynced": "동기화 유지",
"downloadOnce": "한 번만 다운로드",
"keepNUnwatched": "미시청 ${count}개 유지",
"editSyncRule": "동기화 규칙 편집",
"removeSyncRule": "동기화 규칙 제거",
"removeSyncRuleConfirm": "\"${title}\" 동기화를 중단하시겠습니까? 다운로드된 에피소드는 유지됩니다.",
"syncRuleCreated": "동기화 규칙 생성됨 — 미시청 에피소드 ${count}개 유지",
"syncRuleUpdated": "동기화 규칙 업데이트됨",
"syncRuleRemoved": "동기화 규칙 제거됨",
"syncedNewEpisodes": "${title}의 새 에피소드 ${count}개 동기화됨",
"activeSyncRules": "동기화 규칙",
"noSyncRules": "동기화 규칙 없음",
"manageSyncRule": "동기화 관리",
"editEpisodeCount": "에피소드 수",
"editSyncFilter": "동기화 필터",
"syncAllItems": "모든 항목 동기화 중",
"syncUnwatchedItems": "시청하지 않은 항목 동기화 중",
"syncRuleListCreated": "동기화 규칙이 생성되었습니다"
},
"shaders": {
"title": "셰이더",
@@ -716,58 +776,46 @@
"nvscalerDescription": "더 선명한 비디오를 위한 NVIDIA 이미지 스케일링",
"qualityFast": "빠름",
"qualityHQ": "고품질",
"mode": "모드"
"mode": "모드",
"importShader": "셰이더 가져오기",
"customShaderDescription": "사용자 정의 GLSL 셰이더",
"shaderImported": "셰이더를 가져왔습니다",
"shaderImportFailed": "셰이더 가져오기 실패",
"deleteShader": "셰이더 삭제",
"deleteShaderConfirm": "\"${name}\"을(를) 삭제하시겠습니까?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "기기에 연결",
"hostRemoteSession": "원격 세션 호스트",
"controlThisDevice": "휴대폰으로 이 기기를 제어하세요",
"remoteControl": "원격 제어",
"controlDesktop": "데스크톱 기기 제어",
"title": "컴패니언 리모컨",
"connectedTo": "${name}에 연결됨",
"session": {
"creatingSession": "원격 세션 생성 중...",
"failedToCreate": "원격 세션 생성 실패:",
"noSession": "사용 가능한 세션이 없습니다",
"scanQrCode": "QR 코드 스캔",
"orEnterManually": "또는 수동으로 입력",
"startingServer": "원격 서버 시작 중...",
"failedToCreate": "원격 서버를 시작하지 못했습니다:",
"hostAddress": "호스트 주소",
"sessionId": "세션 ID",
"pin": "PIN",
"connected": "연결됨",
"waitingForConnection": "연결 대기 중...",
"serverRunning": "원격 서버 활성",
"serverStopped": "원격 서버 중지됨",
"serverRunningDescription": "네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다",
"serverStoppedDescription": "모바일 기기의 연결을 허용하려면 서버를 시작하세요",
"usePhoneToControl": "모바일 기기로 이 앱을 제어하세요",
"copiedToClipboard": "${label}이(가) 클립보드에 복사되었습니다",
"copyToClipboard": "클립보드에 복사",
"newSession": "새 세션",
"startServer": "서버 시작",
"stopServer": "서버 중지",
"minimize": "최소화"
},
"pairing": {
"scan": "스캔",
"manual": "수동",
"pairWithDesktop": "데스크톱과 페어링",
"enterSessionDetails": "데스크톱 기기에 표시된 세션 정보를 입력하세요",
"discoveryDescription": "같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "8자리 세션 ID 입력",
"pinHint": "6자리 PIN 입력",
"connecting": "연결 중...",
"tips": "팁",
"tipDesktop": "데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요",
"tipScan": "스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다",
"tipWifi": "두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요",
"cameraPermissionRequired": "QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.",
"cameraError": "카메라를 시작할 수 없습니다: ${error}",
"scanInstruction": "데스크톱에 표시된 QR 코드에 카메라를 향하세요",
"invalidQrCode": "유효하지 않은 QR 코드 형식",
"searchingForDevices": "기기 검색 중...",
"noDevicesFound": "네트워크에서 기기를 찾을 수 없습니다",
"noDevicesHint": "데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요",
"availableDevices": "사용 가능한 기기",
"manualConnection": "수동 연결",
"cryptoInitFailed": "보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.",
"validationHostRequired": "호스트 주소를 입력하세요",
"validationHostFormat": "IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)",
"validationSessionIdRequired": "세션 ID를 입력하세요",
"validationSessionIdLength": "세션 ID는 8자리여야 합니다",
"validationPinRequired": "PIN을 입력하세요",
"validationPinLength": "PIN은 6자리여야 합니다",
"connectionTimedOut": "연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.",
"sessionNotFound": "세션을 찾을 수 없습니다. 자격 증명을 확인하세요.",
"validationHostFormat": "형식은 IP:포트야 합니다 (예: 192.168.1.100:48632)",
"connectionTimedOut": "연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.",
"sessionNotFound": "기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.",
"authFailed": "인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.",
"failedToConnect": "연결 실패: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "재연결 중...",
"attemptOf": "${current}/5 시도 중",
"retryNow": "지금 재시도",
"connectionError": "연결 오류",
"notConnected": "연결되지 않음",
"tabRemote": "리모컨",
"tabPlay": "재생",
"tabMore": "더 보기",
@@ -819,13 +865,13 @@
"useExternalPlayer": "외부 플레이어 사용",
"useExternalPlayerDescription": "내장 플레이어 대신 외부 앱에서 동영상 열기",
"selectPlayer": "플레이어 선택",
"customPlayers": "사용자 정의 플레이어",
"systemDefault": "시스템 기본값",
"addCustomPlayer": "사용자 정의 플레이어 추가",
"playerName": "플레이어 이름",
"playerCommand": "명령어",
"playerPackage": "패키지 이름",
"playerUrlScheme": "URL 스킴",
"customPlayer": "사용자 정의 플레이어",
"off": "꺼짐",
"launchFailed": "외부 플레이어를 열 수 없습니다",
"appNotInstalled": "${name}이(가) 설치되어 있지 않습니다",
@@ -847,8 +893,12 @@
"summary": "줄거리",
"poster": "포스터",
"background": "배경",
"logo": "로고",
"squareArt": "정사각형 아트",
"selectPoster": "포스터 선택",
"selectBackground": "배경 선택",
"selectLogo": "로고 선택",
"selectSquareArt": "정사각형 아트 선택",
"fromUrl": "URL에서",
"uploadFile": "파일 업로드",
"enterImageUrl": "이미지 URL 입력",
@@ -890,6 +940,39 @@
"subtitleMode": "자막 자동 선택 모드",
"manuallySelected": "수동 선택",
"shownWithForeignAudio": "외국어 오디오 시 표시",
"alwaysEnabled": "항상 활성화"
"alwaysEnabled": "항상 활성화",
"tags": "태그",
"addTag": "태그 추가",
"genre": "장르",
"director": "감독",
"writer": "작가",
"producer": "프로듀서",
"country": "국가",
"collection": "컬렉션",
"label": "라벨",
"style": "스타일",
"mood": "분위기"
},
"serverTasks": {
"title": "서버 작업",
"failedToLoad": "작업을 불러올 수 없습니다",
"noTasks": "실행 중인 작업 없음"
},
"trakt": {
"title": "Trakt",
"connected": "연결됨",
"connectedAs": "@${username}(으)로 연결됨",
"disconnectConfirm": "Trakt 계정 연결을 끊으시겠습니까?",
"disconnectConfirmBody": "Plezy가 Trakt로 재생 이벤트를 보내지 않습니다. 언제든지 다시 연결할 수 있습니다.",
"scrobble": "실시간 스크로블",
"scrobbleDescription": "재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.",
"watchedSync": "시청 상태 동기화",
"watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.",
"deviceCodeTitle": "Trakt에서 Plezy 활성화",
"deviceCodeBody": "${url}을(를) 방문하여 이 코드를 입력하세요:",
"openTraktActivate": "활성화하려면 Trakt 열기",
"waitingForAuthorization": "인증 대기 중…",
"codeCopied": "코드가 복사됨",
"connectFailed": "Trakt에 연결할 수 없습니다. 다시 시도하세요."
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Logg inn med Plex",
"showQRCode": "Vis QR-kode",
"authenticate": "Autentiser",
"authenticationTimeout": "Autentiseringen ble tidsavbrutt. Prøv igjen.",
"scanQRToSignIn": "Skann denne QR-koden for å logge inn",
"waitingForAuth": "Venter på autentisering...\nFullfør innloggingen i nettleseren din.",
"useBrowser": "Bruk nettleser"
},
"common": {
"cancel": "Avbryt",
"save": "Lagre",
"close": "Lukk",
"clear": "Tøm",
"reset": "Tilbakestill",
"later": "Senere",
"submit": "Send inn",
"confirm": "Bekreft",
"retry": "Prøv igjen",
"logout": "Logg ut",
"unknown": "Ukjent",
"refresh": "Oppdater",
"yes": "Ja",
"no": "Nei",
"delete": "Slett",
"shuffle": "Tilfeldig",
"addTo": "Legg til i...",
"createNew": "Opprett ny",
"connect": "Koble til",
"disconnect": "Koble fra",
"play": "Spill av",
"pause": "Pause",
"resume": "Gjenoppta",
"error": "Feil",
"search": "Søk",
"home": "Hjem",
"back": "Tilbake",
"settings": "Innstillinger",
"mute": "Demp",
"ok": "OK",
"reconnect": "Koble til på nytt",
"exitConfirmTitle": "Avslutte appen?",
"exitConfirmMessage": "Er du sikker på at du vil avslutte?",
"dontAskAgain": "Ikke spør igjen",
"exit": "Avslutt",
"viewAll": "Vis alle",
"checkingNetwork": "Sjekker nettverk...",
"refreshingServers": "Oppdaterer servere...",
"loadingServers": "Laster servere...",
"connectingToServers": "Kobler til servere...",
"startingOfflineMode": "Starter frakoblet modus...",
"loading": "Laster..."
},
"screens": {
"licenses": "Lisenser",
"switchProfile": "Bytt profil",
"subtitleStyling": "Undertekststil",
"mpvConfig": "mpv.conf",
"logs": "Logger"
},
"update": {
"available": "Oppdatering tilgjengelig",
"versionAvailable": "Versjon ${version} er tilgjengelig",
"currentVersion": "Gjeldende: ${version}",
"skipVersion": "Hopp over denne versjonen",
"viewRelease": "Vis utgivelse",
"latestVersion": "Du har den nyeste versjonen",
"checkFailed": "Kunne ikke se etter oppdateringer"
},
"settings": {
"title": "Innstillinger",
"supportDeveloper": "Støtt Plezy",
"supportDeveloperDescription": "Doner via Liberapay for å finansiere utviklingen",
"language": "Språk",
"theme": "Tema",
"appearance": "Utseende",
"videoPlayback": "Videoavspilling",
"videoPlaybackDescription": "Konfigurer avspillingsatferd",
"advanced": "Avansert",
"episodePosterMode": "Episodeplakatstil",
"seriesPoster": "Serieplakat",
"seasonPoster": "Sesongplakat",
"episodeThumbnail": "Miniatyrbilde",
"showHeroSectionDescription": "Vis fremhevet innholdskarusell på hjemmeskjermen",
"secondsLabel": "Sekunder",
"minutesLabel": "Minutter",
"secondsShort": "s",
"minutesShort": "m",
"durationHint": "Angi varighet (${min}-${max})",
"systemTheme": "System",
"lightTheme": "Lyst",
"darkTheme": "Mørkt",
"oledTheme": "OLED",
"libraryDensity": "Bibliotekets tetthet",
"compact": "Kompakt",
"comfortable": "Komfortabel",
"viewMode": "Visningsmodus",
"gridView": "Rutenett",
"listView": "Liste",
"showHeroSection": "Vis fremhevet seksjon",
"useGlobalHubs": "Bruk Plex Home-layout",
"useGlobalHubsDescription": "Vis hjemmeside-huber som den offisielle Plex-klienten. Når av, vises per-bibliotek-anbefalinger i stedet.",
"showServerNameOnHubs": "Vis servernavn på huber",
"showServerNameOnHubsDescription": "Vis alltid servernavnet i hubtitler. Når av, vises kun for dupliserte hubnavn.",
"alwaysKeepSidebarOpen": "Hold sidefeltet alltid åpent",
"alwaysKeepSidebarOpenDescription": "Sidefeltet forblir utvidet og innholdsområdet tilpasser seg",
"showUnwatchedCount": "Vis antall usette",
"showUnwatchedCountDescription": "Vis antall usette episoder på serier og sesonger",
"hideSpoilers": "Skjul spoilere for usette episoder",
"hideSpoilersDescription": "Slør miniatyrbilder og skjul beskrivelser for episoder du ikke har sett ennå",
"playerBackend": "Spillermotor",
"exoPlayer": "ExoPlayer (Anbefalt)",
"mpv": "mpv",
"hardwareDecoding": "Maskinvaredekoding",
"hardwareDecodingDescription": "Bruk maskinvareakselerasjon når tilgjengelig",
"bufferSize": "Bufferstørrelse",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Auto (Anbefalt)",
"bufferSizeWarning": "Enheten din har ${heap}MB minne. En ${size}MB buffer kan forårsake avspillingsproblemer.",
"subtitleStyling": "Undertekststil",
"subtitleStylingDescription": "Tilpass utseendet på undertekster",
"smallSkipDuration": "Kort hoppvarighet",
"largeSkipDuration": "Lang hoppvarighet",
"rewindOnResume": "Spol tilbake ved gjenopptakelse",
"secondsUnit": "${seconds} sekunder",
"defaultSleepTimer": "Standard søvntimer",
"minutesUnit": "${minutes} minutter",
"rememberTrackSelections": "Husk sporvalg per serie/film",
"rememberTrackSelectionsDescription": "Lagre automatisk lyd- og undertekstspråkpreferanser når du bytter spor under avspilling",
"clickVideoTogglesPlayback": "Klikk på video for å veksle avspilling",
"clickVideoTogglesPlaybackDescription": "Hvis aktivert, vil klikk på videospilleren spille av/pause videoen. Ellers vil klikk vise/skjule avspillingskontrollene.",
"videoPlayerControls": "Videospillerkontroller",
"keyboardShortcuts": "Tastatursnarveier",
"keyboardShortcutsDescription": "Tilpass tastatursnarveier",
"videoPlayerNavigation": "Videospillernavigering",
"videoPlayerNavigationDescription": "Bruk piltaster for å navigere videospillerkontroller",
"watchTogetherRelay": "Se Sammen-relay",
"watchTogetherRelayDescription": "Angi en egendefinert relay-server for Se Sammen. Alle deltakere må bruke den samme serveren.",
"watchTogetherRelayHint": "https://min-relay.eksempel.no",
"crashReporting": "Krasjrapportering",
"crashReportingDescription": "Send krasjrapporter for å hjelpe med å forbedre appen",
"debugLogging": "Feilsøkingslogging",
"debugLoggingDescription": "Aktiver detaljert logging for feilsøking",
"viewLogs": "Vis logger",
"viewLogsDescription": "Vis applikasjonslogger",
"clearCache": "Tøm hurtigbuffer",
"clearCacheDescription": "Dette vil tømme alle hurtigbufrede bilder og data. Appen kan bruke lengre tid på å laste innhold etter tømming.",
"clearCacheSuccess": "Hurtigbuffer tømt",
"resetSettings": "Tilbakestill innstillinger",
"resetSettingsDescription": "Dette vil tilbakestille alle innstillinger til standardverdier. Denne handlingen kan ikke angres.",
"resetSettingsSuccess": "Innstillinger tilbakestilt",
"shortcutsReset": "Snarveier tilbakestilt til standard",
"about": "Om",
"aboutDescription": "Appinformasjon og lisenser",
"updates": "Oppdateringer",
"updateAvailable": "Oppdatering tilgjengelig",
"checkForUpdates": "Se etter oppdateringer",
"validationErrorEnterNumber": "Vennligst skriv inn et gyldig tall",
"validationErrorDuration": "Varigheten må være mellom ${min} og ${max} ${unit}",
"shortcutAlreadyAssigned": "Snarvei allerede tilordnet til ${action}",
"shortcutUpdated": "Snarvei oppdatert for ${action}",
"autoSkip": "Automatisk hopp",
"autoSkipIntro": "Hopp over intro automatisk",
"autoSkipIntroDescription": "Hopp automatisk over intromarkører etter noen sekunder",
"autoSkipCredits": "Hopp over rulletekst automatisk",
"autoSkipCreditsDescription": "Hopp automatisk over rulletekst og spill neste episode",
"autoSkipDelay": "Forsinkelse for automatisk hopp",
"autoSkipDelayDescription": "Vent ${seconds} sekunder før automatisk hopping",
"introPattern": "Intromarkørmønster",
"introPatternDescription": "Regulært uttrykk for å gjenkjenne intromarkører i kapitteltitler",
"creditsPattern": "Rulletekstmarkørmønster",
"creditsPatternDescription": "Regulært uttrykk for å gjenkjenne rulletekstmarkører i kapitteltitler",
"invalidRegex": "Ugyldig regulært uttrykk",
"downloads": "Nedlastinger",
"downloadLocationDescription": "Velg hvor nedlastet innhold skal lagres",
"downloadLocationDefault": "Standard (App-lagring)",
"downloadLocationCustom": "Egendefinert plassering",
"selectFolder": "Velg mappe",
"resetToDefault": "Tilbakestill til standard",
"currentPath": "Gjeldende: ${path}",
"downloadLocationChanged": "Nedlastingsplassering endret",
"downloadLocationReset": "Nedlastingsplassering tilbakestilt til standard",
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
"downloadLocationSelectError": "Kunne ikke velge mappe",
"downloadOnWifiOnly": "Last ned kun på WiFi",
"downloadOnWifiOnlyDescription": "Forhindre nedlastinger på mobildata",
"autoRemoveWatchedDownloads": "Fjern sette nedlastinger automatisk",
"autoRemoveWatchedDownloadsDescription": "Slett nedlastede episoder og filmer automatisk når de er markert som sett",
"cellularDownloadBlocked": "Nedlastinger er deaktivert på mobildata. Koble til WiFi eller endre innstillingen.",
"maxVolume": "Maks volum",
"maxVolumeDescription": "Tillat volumforsterkning over 100% for stille media",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Vis hva du ser på Discord",
"trakt": "Trakt",
"traktDescription": "Synkroniser visningshistorikk med Trakt",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Tillat mobilenheter på nettverket ditt å styre denne appen",
"autoPip": "Auto bilde-i-bilde",
"autoPipDescription": "Gå automatisk til bilde-i-bilde når du forlater appen under avspilling",
"matchContentFrameRate": "Tilpass innholdets bildefrekvens",
"matchContentFrameRateDescription": "Juster skjermens oppdateringsfrekvens for å matche videoinnhold, reduserer hakking og sparer batteri",
"matchRefreshRate": "Tilpass oppdateringsfrekvens",
"matchRefreshRateDescription": "Bytt skjermens oppdateringsfrekvens for å matche videoinnhold i fullskjerm",
"matchDynamicRange": "Tilpass dynamisk område",
"matchDynamicRangeDescription": "Aktiver HDR automatisk for HDR-innhold og gå tilbake til SDR når spilleren lukkes",
"displaySwitchDelay": "Forsinkelse ved skjermbytte",
"tunneledPlayback": "Tunnelert avspilling",
"tunneledPlaybackDescription": "Bruk maskinvareakselerert videotunnelering. Deaktiver hvis du ser svart skjerm med lyd på HDR-innhold",
"requireProfileSelectionOnOpen": "Spør om profil ved appåpning",
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åpnes",
"confirmExitOnBack": "Bekreft før avslutning",
"confirmExitOnBackDescription": "Vis en bekreftelsesdialog når du trykker tilbake for å avslutte appen",
"forceTvMode": "Tving TV-modus",
"forceTvModeDescription": "Bruk TV-oppsettet uavhengig av automatisk gjenkjenning. Nyttig på Android TV-enheter som ikke rapporterer leanback-funksjonen. Starter appen på nytt ved endring.",
"autoHidePerformanceOverlay": "Skjul ytelsesoverlegg automatisk",
"autoHidePerformanceOverlayDescription": "Fade ytelsesoverlegget med avspillingskontrollene",
"showNavBarLabels": "Vis navigasjonsfeltlabeler",
"showNavBarLabelsDescription": "Vis tekstlabeler under navigasjonsfeltikoner",
"liveTvDefaultFavorites": "Standard til favorittkanaler",
"liveTvDefaultFavoritesDescription": "Vis kun favorittkanaler når du åpner Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Søk i filmer, serier, musikk...",
"tryDifferentTerm": "Prøv et annet søkeord",
"searchYourMedia": "Søk i mediene dine",
"enterTitleActorOrKeyword": "Skriv inn tittel, skuespiller eller nøkkelord"
},
"hotkeys": {
"setShortcutFor": "Angi snarvei for ${actionName}",
"clearShortcut": "Fjern snarvei",
"actions": {
"playPause": "Spill av/Pause",
"volumeUp": "Volum opp",
"volumeDown": "Volum ned",
"seekForward": "Spol fremover (${seconds}s)",
"seekBackward": "Spol bakover (${seconds}s)",
"fullscreenToggle": "Veksle fullskjerm",
"muteToggle": "Veksle demping",
"subtitleToggle": "Veksle undertekster",
"audioTrackNext": "Neste lydspor",
"subtitleTrackNext": "Neste undertekstspor",
"chapterNext": "Neste kapittel",
"chapterPrevious": "Forrige kapittel",
"speedIncrease": "Øk hastighet",
"speedDecrease": "Reduser hastighet",
"speedReset": "Tilbakestill hastighet",
"subSeekNext": "Spol til neste undertekst",
"subSeekPrev": "Spol til forrige undertekst",
"shaderToggle": "Veksle shadere",
"skipMarker": "Hopp over intro/rulletekst"
}
},
"fileInfo": {
"title": "Filinformasjon",
"video": "Video",
"audio": "Lyd",
"file": "Fil",
"advanced": "Avansert",
"codec": "Kodek",
"resolution": "Oppløsning",
"bitrate": "Bitrate",
"frameRate": "Bildefrekvens",
"aspectRatio": "Sideforhold",
"profile": "Profil",
"bitDepth": "Bitdybde",
"colorSpace": "Fargerom",
"colorRange": "Fargeområde",
"colorPrimaries": "Fargeprimærer",
"chromaSubsampling": "Krominansnedsampling",
"channels": "Kanaler",
"subtitles": "Undertekster",
"overallBitrate": "Total bitrate",
"path": "Sti",
"size": "Størrelse",
"container": "Beholder",
"duration": "Varighet",
"optimizedForStreaming": "Optimalisert for strømming",
"has64bitOffsets": "64-biters forskyvninger"
},
"mediaMenu": {
"markAsWatched": "Merk som sett",
"markAsUnwatched": "Merk som usett",
"removeFromContinueWatching": "Fjern fra Fortsett å se",
"goToSeries": "Gå til serie",
"goToSeason": "Gå til sesong",
"shufflePlay": "Tilfeldig avspilling",
"fileInfo": "Filinformasjon",
"deleteFromServer": "Slett fra server",
"confirmDelete": "Dette vil permanent slette dette mediet og filene fra serveren din. Dette kan ikke angres.",
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
"mediaDeletedSuccessfully": "Medieelement slettet",
"mediaFailedToDelete": "Kunne ikke slette medieelement",
"rate": "Vurder",
"playFromBeginning": "Spill fra begynnelsen",
"playVersion": "Spill av versjon..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
"mediaCardShow": "${title}, TV-serie",
"mediaCardEpisode": "${title}, ${episodeInfo}",
"mediaCardSeason": "${title}, ${seasonInfo}",
"mediaCardWatched": "sett",
"mediaCardPartiallyWatched": "${percent} prosent sett",
"mediaCardUnwatched": "usett",
"tapToPlay": "Trykk for å spille"
},
"tooltips": {
"shufflePlay": "Tilfeldig avspilling",
"playTrailer": "Spill trailer",
"markAsWatched": "Merk som sett",
"markAsUnwatched": "Merk som usett"
},
"videoControls": {
"audioLabel": "Lyd",
"subtitlesLabel": "Undertekster",
"resetToZero": "Tilbakestill til 0ms",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label} spilles senere",
"playsEarlier": "${label} spilles tidligere",
"noOffset": "Ingen forskyvning",
"letterbox": "Letterbox",
"fillScreen": "Fyll skjerm",
"stretch": "Strekk",
"lockRotation": "Lås rotasjon",
"unlockRotation": "Lås opp rotasjon",
"timerActive": "Timer aktiv",
"playbackWillPauseIn": "Avspilling vil pause om ${duration}",
"stillWatching": "Ser du fortsatt?",
"pausingIn": "Pauser om ${seconds}s",
"continueWatching": "Fortsett",
"autoPlayNext": "Spill neste automatisk",
"playNext": "Spill neste",
"playButton": "Spill av",
"pauseButton": "Pause",
"seekBackwardButton": "Spol tilbake ${seconds} sekunder",
"seekForwardButton": "Spol fremover ${seconds} sekunder",
"previousButton": "Forrige episode",
"nextButton": "Neste episode",
"previousChapterButton": "Forrige kapittel",
"nextChapterButton": "Neste kapittel",
"muteButton": "Demp",
"unmuteButton": "Opphev demping",
"settingsButton": "Videoinnstillinger",
"tracksButton": "Lyd og undertekster",
"chaptersButton": "Kapitler",
"versionsButton": "Videoversjoner",
"pipButton": "Bilde-i-bilde-modus",
"aspectRatioButton": "Sideforhold",
"ambientLighting": "Omgivelseslys",
"fullscreenButton": "Gå til fullskjerm",
"exitFullscreenButton": "Avslutt fullskjerm",
"alwaysOnTopButton": "Alltid øverst",
"rotationLockButton": "Rotasjonslås",
"lockScreen": "Lås skjerm",
"screenLockButton": "Skjermlås",
"longPressToUnlock": "Langt trykk for å låse opp",
"timelineSlider": "Videotidslinje",
"volumeSlider": "Volumnivå",
"endsAt": "Slutter kl. ${time}",
"pipActive": "Spiller i bilde-i-bilde",
"pipFailed": "Bilde-i-bilde kunne ikke starte",
"pipErrors": {
"androidVersion": "Krever Android 8.0 eller nyere",
"iosVersion": "Krever iOS 15.0 eller nyere",
"permissionDisabled": "Bilde-i-bilde-tillatelse er deaktivert. Aktiver den i Innstillinger > Apper > Plezy > Bilde-i-bilde",
"notSupported": "Enheten støtter ikke bilde-i-bilde-modus",
"voSwitchFailed": "Kunne ikke bytte videoutgang for bilde-i-bilde",
"failed": "Bilde-i-bilde kunne ikke starte",
"unknown": "En feil oppstod: ${error}"
},
"chapters": "Kapitler",
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
"queue": "Kø",
"noQueueItems": "Ingen elementer i kø",
"searchSubtitles": "Søk etter undertekster",
"language": "Språk",
"noSubtitlesFound": "Ingen undertekster funnet",
"subtitleDownloaded": "Undertekst lastet ned",
"subtitleDownloadFailed": "Kunne ikke laste ned undertekst",
"searchLanguages": "Søk etter språk..."
},
"userStatus": {
"admin": "Administrator",
"restricted": "Begrenset",
"protected": "Beskyttet",
"current": "GJELDENDE"
},
"messages": {
"markedAsWatched": "Merket som sett",
"markedAsUnwatched": "Merket som usett",
"markedAsWatchedOffline": "Merket som sett (synkroniseres når tilkoblet)",
"markedAsUnwatchedOffline": "Merket som usett (synkroniseres når tilkoblet)",
"autoRemovedWatchedDownload": "Automatisk fjernet: ${title}",
"removedFromContinueWatching": "Fjernet fra Fortsett å se",
"errorLoading": "Feil: ${error}",
"fileInfoNotAvailable": "Filinformasjon ikke tilgjengelig",
"errorLoadingFileInfo": "Feil ved lasting av filinformasjon: ${error}",
"errorLoadingSeries": "Feil ved lasting av serie",
"errorLoadingSeason": "Feil ved lasting av sesong",
"musicNotSupported": "Musikkavspilling støttes ikke ennå",
"logsCleared": "Logger tømt",
"logsCopied": "Logger kopiert til utklippstavle",
"noLogsAvailable": "Ingen logger tilgjengelig",
"libraryScanning": "Skanner \"${title}\"...",
"libraryScanStarted": "Bibliotekkanning startet for \"${title}\"",
"libraryScanFailed": "Kunne ikke skanne bibliotek: ${error}",
"metadataRefreshing": "Oppdaterer metadata for \"${title}\"...",
"metadataRefreshStarted": "Metadataoppdatering startet for \"${title}\"",
"metadataRefreshFailed": "Kunne ikke oppdatere metadata: ${error}",
"logoutConfirm": "Er du sikker på at du vil logge ut?",
"noSeasonsFound": "Ingen sesonger funnet",
"noEpisodesFound": "Ingen episoder funnet i første sesong",
"noEpisodesFoundGeneral": "Ingen episoder funnet",
"noResultsFound": "Ingen resultater funnet",
"sleepTimerSet": "Søvntimer satt til ${label}",
"noItemsAvailable": "Ingen elementer tilgjengelig",
"failedToCreatePlayQueueNoItems": "Kunne ikke opprette avspillingskø ingen elementer",
"failedPlayback": "Kunne ikke ${action}: ${error}",
"switchingToCompatiblePlayer": "Bytter til kompatibel spiller...",
"serverLimitTitle": "Avspilling mislyktes",
"serverLimitBody": "Serveren returnerte en feil (HTTP 500). Det betyr vanligvis at servereieren har satt en båndbredde- eller transkodingsgrense som avviser økten din. Det er ingenting du kan gjøre fra klienten — servereieren må justere innstillingene sine.",
"logsUploaded": "Logger lastet opp",
"logsUploadFailed": "Kunne ikke laste opp logger",
"logId": "Logg-ID"
},
"subtitlingStyling": {
"text": "Tekst",
"border": "Kantlinje",
"background": "Bakgrunn",
"fontSize": "Skriftstørrelse",
"textColor": "Tekstfarge",
"borderSize": "Kantstørrelse",
"borderColor": "Kantfarge",
"backgroundOpacity": "Bakgrunnsopasitet",
"backgroundColor": "Bakgrunnsfarge",
"position": "Posisjon",
"assOverride": "ASS-overstyring",
"bold": "Fet",
"italic": "Kursiv"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "Avanserte videospillerinnstillinger",
"presets": "Forhåndsinnstillinger",
"noPresets": "Ingen lagrede forhåndsinnstillinger",
"saveAsPreset": "Lagre som forhåndsinnstilling...",
"presetName": "Forhåndsinnstillingsnavn",
"presetNameHint": "Skriv inn et navn for denne forhåndsinnstillingen",
"loadPreset": "Last inn",
"deletePreset": "Slett",
"presetSaved": "Forhåndsinnstilling lagret",
"presetLoaded": "Forhåndsinnstilling lastet inn",
"presetDeleted": "Forhåndsinnstilling slettet",
"confirmDeletePreset": "Er du sikker på at du vil slette denne forhåndsinnstillingen?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# kommentar"
},
"dialog": {
"confirmAction": "Bekreft handling"
},
"discover": {
"title": "Oppdag",
"switchProfile": "Bytt profil",
"noContentAvailable": "Ingen innhold tilgjengelig",
"addMediaToLibraries": "Legg til medier i bibliotekene dine",
"continueWatching": "Fortsett å se",
"playEpisode": "S${season}E${episode}",
"overview": "Oversikt",
"cast": "Skuespillere",
"extras": "Trailere og ekstra",
"studio": "Studio",
"rating": "Vurdering",
"movie": "Film",
"tvShow": "TV-serie",
"minutesLeft": "${minutes} min igjen"
},
"errors": {
"searchFailed": "Søk mislyktes: ${error}",
"connectionTimeout": "Tidsavbrudd ved lasting av ${context}",
"connectionFailed": "Kunne ikke koble til Plex-server",
"failedToLoad": "Kunne ikke laste ${context}: ${error}",
"noClientAvailable": "Ingen klient tilgjengelig",
"authenticationFailed": "Autentisering mislyktes: ${error}",
"couldNotLaunchUrl": "Kunne ikke åpne autentiserings-URL",
"pleaseEnterToken": "Vennligst skriv inn et token",
"invalidToken": "Ugyldig token",
"failedToVerifyToken": "Kunne ikke verifisere token: ${error}",
"failedToSwitchProfile": "Kunne ikke bytte til ${displayName}"
},
"libraries": {
"title": "Biblioteker",
"scanLibraryFiles": "Skann bibliotekfiler",
"scanLibrary": "Skann bibliotek",
"analyze": "Analyser",
"analyzeLibrary": "Analyser bibliotek",
"refreshMetadata": "Oppdater metadata",
"emptyTrash": "Tøm papirkurv",
"emptyingTrash": "Tømmer papirkurv for \"${title}\"...",
"trashEmptied": "Papirkurv tømt for \"${title}\"",
"failedToEmptyTrash": "Kunne ikke tømme papirkurv: ${error}",
"analyzing": "Analyserer \"${title}\"...",
"analysisStarted": "Analyse startet for \"${title}\"",
"failedToAnalyze": "Kunne ikke analysere bibliotek: ${error}",
"noLibrariesFound": "Ingen biblioteker funnet",
"thisLibraryIsEmpty": "Dette biblioteket er tomt",
"all": "Alle",
"clearAll": "Tøm alle",
"scanLibraryConfirm": "Er du sikker på at du vil skanne \"${title}\"?",
"analyzeLibraryConfirm": "Er du sikker på at du vil analysere \"${title}\"?",
"refreshMetadataConfirm": "Er du sikker på at du vil oppdatere metadata for \"${title}\"?",
"emptyTrashConfirm": "Er du sikker på at du vil tømme papirkurven for \"${title}\"?",
"manageLibraries": "Administrer biblioteker",
"sort": "Sorter",
"sortBy": "Sorter etter",
"filters": "Filtre",
"confirmActionMessage": "Er du sikker på at du vil utføre denne handlingen?",
"showLibrary": "Vis bibliotek",
"hideLibrary": "Skjul bibliotek",
"libraryOptions": "Bibliotekalternativer",
"content": "bibliotekinnhold",
"selectLibrary": "Velg bibliotek",
"filtersWithCount": "Filtre (${count})",
"noRecommendations": "Ingen anbefalinger tilgjengelig",
"noCollections": "Ingen samlinger i dette biblioteket",
"noFoldersFound": "Ingen mapper funnet",
"folders": "mapper",
"tabs": {
"recommended": "Anbefalt",
"browse": "Bla gjennom",
"collections": "Samlinger",
"playlists": "Spillelister"
},
"groupings": {
"title": "Gruppering",
"all": "Alle",
"movies": "Filmer",
"shows": "TV-serier",
"seasons": "Sesonger",
"episodes": "Episoder",
"folders": "Mapper"
}
},
"about": {
"title": "Om",
"openSourceLicenses": "Åpen kildekode-lisenser",
"versionLabel": "Versjon ${version}",
"appDescription": "En vakker Plex-klient for Flutter",
"viewLicensesDescription": "Vis lisenser for tredjepartsbiblioteker"
},
"serverSelection": {
"allServerConnectionsFailed": "Kunne ikke koble til noen servere. Sjekk nettverket ditt og prøv igjen.",
"noServersFoundForAccount": "Ingen servere funnet for ${username} (${email})",
"failedToLoadServers": "Kunne ikke laste servere: ${error}"
},
"hubDetail": {
"title": "Tittel",
"releaseYear": "Utgivelsesår",
"dateAdded": "Dato lagt til",
"rating": "Vurdering",
"noItemsFound": "Ingen elementer funnet"
},
"logs": {
"clearLogs": "Tøm logger",
"copyLogs": "Kopier logger",
"uploadLogs": "Last opp logger"
},
"licenses": {
"relatedPackages": "Relaterte pakker",
"license": "Lisens",
"licenseNumber": "Lisens ${number}",
"licensesCount": "${count} lisenser"
},
"navigation": {
"libraries": "Biblioteker",
"downloads": "Nedlastinger",
"liveTv": "Direkte-TV"
},
"liveTv": {
"title": "Direkte-TV",
"guide": "Programguide",
"noChannels": "Ingen kanaler tilgjengelig",
"noDvr": "Ingen DVR konfigurert på noen server",
"noPrograms": "Ingen programdata tilgjengelig",
"live": "DIREKTE",
"reloadGuide": "Last inn programguide på nytt",
"now": "Nå",
"today": "I dag",
"midnight": "Midnatt",
"overnight": "Natt",
"morning": "Morgen",
"daytime": "Dagtid",
"evening": "Kveld",
"lateNight": "Sen kveld",
"whatsOn": "Hva går nå",
"watchChannel": "Se kanal",
"favorites": "Favoritter",
"reorderFavorites": "Endre rekkefølge på favoritter",
"joinSession": "Bli med i pågående økt",
"watchFromStart": "Se fra starten (${minutes} min siden)",
"watchLive": "Se direkte",
"goToLive": "Gå til direkte"
},
"collections": {
"title": "Samlinger",
"collection": "Samling",
"empty": "Samlingen er tom",
"unknownLibrarySection": "Kan ikke slette: Ukjent bibliotekseksjon",
"deleteCollection": "Slett samling",
"deleteConfirm": "Er du sikker på at du vil slette \"${title}\"? Denne handlingen kan ikke angres.",
"deleted": "Samling slettet",
"deleteFailed": "Kunne ikke slette samling",
"deleteFailedWithError": "Kunne ikke slette samling: ${error}",
"failedToLoadItems": "Kunne ikke laste samlingselementer: ${error}",
"selectCollection": "Velg samling",
"collectionName": "Samlingsnavn",
"enterCollectionName": "Skriv inn samlingsnavn",
"addedToCollection": "Lagt til i samling",
"errorAddingToCollection": "Kunne ikke legge til i samling",
"created": "Samling opprettet",
"removeFromCollection": "Fjern fra samling",
"removeFromCollectionConfirm": "Fjerne \"${title}\" fra denne samlingen?",
"removedFromCollection": "Fjernet fra samling",
"removeFromCollectionFailed": "Kunne ikke fjerne fra samling",
"removeFromCollectionError": "Feil ved fjerning fra samling: ${error}",
"searchCollections": "Søk i samlinger..."
},
"playlists": {
"title": "Spillelister",
"playlist": "Spilleliste",
"noPlaylists": "Ingen spillelister funnet",
"create": "Opprett spilleliste",
"playlistName": "Spillelistenavn",
"enterPlaylistName": "Skriv inn spillelistenavn",
"delete": "Slett spilleliste",
"removeItem": "Fjern fra spilleliste",
"smartPlaylist": "Smart spilleliste",
"itemCount": "${count} elementer",
"oneItem": "1 element",
"emptyPlaylist": "Denne spillelisten er tom",
"deleteConfirm": "Slett spilleliste?",
"deleteMessage": "Er du sikker på at du vil slette \"${name}\"?",
"created": "Spilleliste opprettet",
"deleted": "Spilleliste slettet",
"itemAdded": "Lagt til i spilleliste",
"itemRemoved": "Fjernet fra spilleliste",
"selectPlaylist": "Velg spilleliste",
"errorCreating": "Kunne ikke opprette spilleliste",
"errorDeleting": "Kunne ikke slette spilleliste",
"errorLoading": "Kunne ikke laste spillelister",
"errorAdding": "Kunne ikke legge til i spilleliste",
"errorReordering": "Kunne ikke omorganisere spillelisteelement",
"errorRemoving": "Kunne ikke fjerne fra spilleliste"
},
"watchTogether": {
"title": "Se sammen",
"description": "Se innhold synkronisert med venner og familie",
"createSession": "Opprett økt",
"creating": "Oppretter...",
"joinSession": "Bli med i økt",
"joining": "Blir med...",
"controlMode": "Kontrollmodus",
"controlModeQuestion": "Hvem kan kontrollere avspilling?",
"hostOnly": "Kun vert",
"anyone": "Alle",
"hostingSession": "Er vert for økt",
"inSession": "I økt",
"sessionCode": "Øktkode",
"hostControlsPlayback": "Verten kontrollerer avspilling",
"anyoneCanControl": "Alle kan kontrollere avspilling",
"hostControls": "Vertskontroll",
"anyoneControls": "Alle kontrollerer",
"participants": "Deltakere",
"host": "Vert",
"hostBadge": "VERT",
"youAreHost": "Du er verten",
"watchingWithOthers": "Ser med andre",
"endSession": "Avslutt økt",
"leaveSession": "Forlat økt",
"endSessionQuestion": "Avslutte økt?",
"leaveSessionQuestion": "Forlate økt?",
"endSessionConfirm": "Dette vil avslutte økten for alle deltakere.",
"leaveSessionConfirm": "Du vil bli fjernet fra økten.",
"endSessionConfirmOverlay": "Dette vil avslutte se sammen-økten for alle deltakere.",
"leaveSessionConfirmOverlay": "Du vil bli frakoblet fra se sammen-økten.",
"end": "Avslutt",
"leave": "Forlat",
"syncing": "Synkroniserer...",
"joinWatchSession": "Bli med i se sammen-økt",
"enterCodeHint": "Skriv inn 5-tegns kode",
"pasteFromClipboard": "Lim inn fra utklippstavle",
"pleaseEnterCode": "Vennligst skriv inn en øktkode",
"codeMustBe5Chars": "Øktkoden må være 5 tegn",
"joinInstructions": "Skriv inn øktkoden delt av verten for å bli med i se sammen-økten.",
"failedToCreate": "Kunne ikke opprette økt",
"failedToJoin": "Kunne ikke bli med i økt",
"sessionCodeCopied": "Øktkode kopiert til utklippstavle",
"relayUnreachable": "Reléserveren er utilgjengelig. Dette kan skyldes at internettleverandøren din blokkerer tilkoblingen. Du kan fortsatt prøve, men Se sammen fungerer kanskje ikke.",
"reconnectingToHost": "Kobler til verten på nytt...",
"currentPlayback": "Gjeldende avspilling",
"joinCurrentPlayback": "Bli med i gjeldende avspilling",
"joinCurrentPlaybackDescription": "Hopp tilbake til det verten ser på nå",
"failedToOpenCurrentPlayback": "Kunne ikke åpne gjeldende avspilling",
"participantJoined": "${name} ble med",
"participantLeft": "${name} forlot",
"participantPaused": "${name} pauset",
"participantResumed": "${name} gjenopptok",
"participantSeeked": "${name} spolet",
"participantBuffering": "${name} buffrer",
"waitingForParticipants": "Venter på at andre laster inn...",
"recentRooms": "Nylige rom",
"renameRoom": "Gi nytt navn til rom",
"removeRoom": "Fjern"
},
"downloads": {
"title": "Nedlastinger",
"manage": "Administrer",
"tvShows": "TV-serier",
"movies": "Filmer",
"noDownloads": "Ingen nedlastinger ennå",
"noDownloadsDescription": "Nedlastet innhold vil vises her for frakoblet visning",
"downloadNow": "Last ned",
"deleteDownload": "Slett nedlasting",
"retryDownload": "Prøv nedlasting på nytt",
"downloadQueued": "Nedlasting i kø",
"serverErrorBitrate": "Serverfeil — filen overskrider muligens grensen for fjernstrømming-bitrate",
"episodesQueued": "${count} episoder i nedlastingskø",
"downloadDeleted": "Nedlasting slettet",
"deleteConfirm": "Er du sikker på at du vil slette \"${title}\"? Dette vil fjerne den nedlastede filen fra enheten din.",
"deletingWithProgress": "Sletter ${title}... (${current} av ${total})",
"noDownloadsTree": "Ingen nedlastinger",
"pauseAll": "Pause alle",
"resumeAll": "Gjenoppta alle",
"deleteAll": "Slett alle",
"selectVersion": "Velg versjon",
"allEpisodes": "Alle episoder",
"unwatchedOnly": "Kun usette",
"nextNUnwatched": "Neste ${count} usette",
"customAmount": "Egendefinert antall...",
"howManyEpisodes": "Hvor mange episoder?",
"itemsQueued": "${count} elementer i nedlastingskø",
"keepSynced": "Hold synkronisert",
"downloadOnce": "Last ned én gang",
"keepNUnwatched": "Behold ${count} usette",
"editSyncRule": "Rediger synkroniseringsregel",
"removeSyncRule": "Fjern synkroniseringsregel",
"removeSyncRuleConfirm": "Slutte å synkronisere \"${title}\"? Nedlastede episoder beholdes.",
"syncRuleCreated": "Synkroniseringsregel opprettet — beholder ${count} usette episoder",
"syncRuleUpdated": "Synkroniseringsregel oppdatert",
"syncRuleRemoved": "Synkroniseringsregel fjernet",
"syncedNewEpisodes": "Synkroniserte ${count} nye episoder for ${title}",
"activeSyncRules": "Synkroniseringsregler",
"noSyncRules": "Ingen synkroniseringsregler",
"manageSyncRule": "Administrer synkronisering",
"editEpisodeCount": "Antall episoder",
"editSyncFilter": "Synkroniseringsfilter",
"syncAllItems": "Synkroniserer alle elementer",
"syncUnwatchedItems": "Synkroniserer usette elementer",
"syncRuleListCreated": "Synkroniseringsregel opprettet"
},
"shaders": {
"title": "Shadere",
"noShaderDescription": "Ingen videoforbedring",
"nvscalerDescription": "NVIDIA bildeskalering for skarpere video",
"qualityFast": "Rask",
"qualityHQ": "Høy kvalitet",
"mode": "Modus",
"importShader": "Importer shader",
"customShaderDescription": "Egendefinert GLSL-shader",
"shaderImported": "Shader importert",
"shaderImportFailed": "Kunne ikke importere shader",
"deleteShader": "Slett shader",
"deleteShaderConfirm": "Slette \"${name}\"?"
},
"companionRemote": {
"title": "Følgesvenn-fjernkontroll",
"connectedTo": "Tilkoblet ${name}",
"session": {
"startingServer": "Starter fjernserver...",
"failedToCreate": "Kunne ikke starte fjernserver:",
"hostAddress": "Vertsadresse",
"connected": "Tilkoblet",
"serverRunning": "Fjernserver aktiv",
"serverStopped": "Fjernserver stoppet",
"serverRunningDescription": "Mobilenheter på nettverket ditt kan oppdage og koble til denne appen",
"serverStoppedDescription": "Start serveren for å la mobilenheter koble til",
"usePhoneToControl": "Bruk mobilenheten din til å styre denne appen",
"startServer": "Start server",
"stopServer": "Stopp server",
"minimize": "Minimer"
},
"pairing": {
"discoveryDescription": "Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "Kobler til...",
"searchingForDevices": "Søker etter enheter...",
"noDevicesFound": "Ingen enheter funnet på nettverket ditt",
"noDevicesHint": "Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk",
"availableDevices": "Tilgjengelige enheter",
"manualConnection": "Manuell tilkobling",
"cryptoInitFailed": "Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.",
"validationHostRequired": "Vennligst oppgi vertsadresse",
"validationHostFormat": "Format må være IP:port (f.eks. 192.168.1.100:48632)",
"connectionTimedOut": "Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.",
"sessionNotFound": "Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.",
"authFailed": "Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.",
"failedToConnect": "Kunne ikke koble til: ${error}"
},
"remote": {
"disconnectConfirm": "Vil du koble fra fjernøkten?",
"reconnecting": "Kobler til på nytt...",
"attemptOf": "Forsøk ${current} av 5",
"retryNow": "Prøv nå",
"tabRemote": "Fjernkontroll",
"tabPlay": "Spill av",
"tabMore": "Mer",
"menu": "Meny",
"tabNavigation": "Fanenavigering",
"tabDiscover": "Oppdag",
"tabLibraries": "Biblioteker",
"tabSearch": "Søk",
"tabDownloads": "Nedlastinger",
"tabSettings": "Innstillinger",
"previous": "Forrige",
"playPause": "Spill av/Pause",
"next": "Neste",
"seekBack": "Spol tilbake",
"stop": "Stopp",
"seekForward": "Spol fremover",
"volume": "Volum",
"volumeDown": "Ned",
"volumeUp": "Opp",
"fullscreen": "Fullskjerm",
"subtitles": "Undertekster",
"audio": "Lyd",
"searchHint": "Søk på stasjonær..."
}
},
"videoSettings": {
"playbackSettings": "Avspillingsinnstillinger",
"playbackSpeed": "Avspillingshastighet",
"sleepTimer": "Søvntimer",
"audioSync": "Lydsynkronisering",
"subtitleSync": "Undertekstsynkronisering",
"hdr": "HDR",
"audioOutput": "Lydutgang",
"performanceOverlay": "Ytelsesoverlegg",
"audioPassthrough": "Lydgjennomgang",
"audioNormalization": "Lydnormalisering"
},
"externalPlayer": {
"title": "Ekstern spiller",
"useExternalPlayer": "Bruk ekstern spiller",
"useExternalPlayerDescription": "Åpne videoer i en ekstern app i stedet for den innebygde spilleren",
"selectPlayer": "Velg spiller",
"customPlayers": "Egendefinerte spillere",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Legg til egendefinert spiller",
"playerName": "Spillernavn",
"playerCommand": "Kommando",
"playerPackage": "Pakkenavn",
"playerUrlScheme": "URL-skjema",
"off": "Av",
"launchFailed": "Kunne ikke åpne ekstern spiller",
"appNotInstalled": "${name} er ikke installert",
"playInExternalPlayer": "Spill av i ekstern spiller"
},
"metadataEdit": {
"editMetadata": "Rediger...",
"screenTitle": "Rediger metadata",
"basicInfo": "Grunnleggende info",
"artwork": "Kunstverk",
"advancedSettings": "Avanserte innstillinger",
"title": "Tittel",
"sortTitle": "Sorteringsstittel",
"originalTitle": "Originaltittel",
"releaseDate": "Utgivelsesdato",
"contentRating": "Innholdsvurdering",
"studio": "Studio",
"tagline": "Slagord",
"summary": "Sammendrag",
"poster": "Plakat",
"background": "Bakgrunn",
"logo": "Logo",
"squareArt": "Kvadratisk bilde",
"selectPoster": "Velg plakat",
"selectBackground": "Velg bakgrunn",
"selectLogo": "Velg logo",
"selectSquareArt": "Velg kvadratisk bilde",
"fromUrl": "Fra URL",
"uploadFile": "Last opp fil",
"enterImageUrl": "Skriv inn bilde-URL",
"imageUrl": "Bilde-URL",
"metadataUpdated": "Metadata oppdatert",
"metadataUpdateFailed": "Kunne ikke oppdatere metadata",
"artworkUpdated": "Kunstverk oppdatert",
"artworkUpdateFailed": "Kunne ikke oppdatere kunstverk",
"noArtworkAvailable": "Ingen kunstverk tilgjengelig",
"notSet": "Ikke angitt",
"libraryDefault": "Bibliotekstandard",
"accountDefault": "Kontostandard",
"seriesDefault": "Seriestandard",
"episodeSorting": "Episodesortering",
"oldestFirst": "Eldste først",
"newestFirst": "Nyeste først",
"keep": "Behold",
"allEpisodes": "Alle episoder",
"latestEpisodes": "${count} nyeste episoder",
"latestEpisode": "Nyeste episode",
"episodesAddedPastDays": "Episoder lagt til de siste ${count} dagene",
"deleteAfterPlaying": "Slett episoder etter avspilling",
"never": "Aldri",
"afterADay": "Etter en dag",
"afterAWeek": "Etter en uke",
"afterAMonth": "Etter en måned",
"onNextRefresh": "Ved neste oppdatering",
"seasons": "Sesonger",
"show": "Vis",
"hide": "Skjul",
"episodeOrdering": "Episoderekkefølge",
"tmdbAiring": "The Movie Database (Sendt)",
"tvdbAiring": "TheTVDB (Sendt)",
"tvdbAbsolute": "TheTVDB (Absolutt)",
"metadataLanguage": "Metadataspråk",
"useOriginalTitle": "Bruk originaltittel",
"preferredAudioLanguage": "Foretrukket lydspråk",
"preferredSubtitleLanguage": "Foretrukket undertekstspråk",
"subtitleMode": "Automatisk valg av undertekstmodus",
"manuallySelected": "Manuelt valgt",
"shownWithForeignAudio": "Vist med fremmedspråklig lyd",
"alwaysEnabled": "Alltid aktivert",
"tags": "Tagger",
"addTag": "Legg til tagg",
"genre": "Sjanger",
"director": "Regissør",
"writer": "Forfatter",
"producer": "Produsent",
"country": "Land",
"collection": "Samling",
"label": "Etikett",
"style": "Stil",
"mood": "Stemning"
},
"serverTasks": {
"title": "Serveroppgaver",
"failedToLoad": "Kunne ikke laste oppgaver",
"noTasks": "Ingen oppgaver kjører"
},
"trakt": {
"title": "Trakt",
"connected": "Tilkoblet",
"connectedAs": "Tilkoblet som @${username}",
"disconnectConfirm": "Koble fra Trakt-konto?",
"disconnectConfirmBody": "Plezy slutter å sende avspillingshendelser til Trakt. Du kan koble til igjen når som helst.",
"scrobble": "Sanntids-scrobbling",
"scrobbleDescription": "Send avspillings-, pause- og stopphendelser til Trakt under avspilling.",
"watchedSync": "Synkroniser sett-status",
"watchedSyncDescription": "Når du markerer noe som sett i Plezy, markeres det også på Trakt.",
"deviceCodeTitle": "Aktiver Plezy på Trakt",
"deviceCodeBody": "Besøk ${url} og skriv inn denne koden:",
"openTraktActivate": "Åpne Trakt for å aktivere",
"waitingForAuthorization": "Venter på autorisering…",
"codeCopied": "Kode kopiert",
"connectFailed": "Kunne ikke koble til Trakt. Prøv igjen."
}
}
+249 -166
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Inloggen met Plex",
"showQRCode": "Toon QR-code",
"authenticate": "Authenticeren",
"debugEnterToken": "Debug: Voer Plex Token in",
"plexTokenLabel": "Plex Authenticatietoken",
"plexTokenHint": "Voer je Plex.tv token in",
"authenticationTimeout": "Authenticatie verlopen. Probeer opnieuw.",
"scanQRToSignIn": "Scan deze QR-code om in te loggen",
"waitingForAuth": "Wachten op authenticatie...\nVoltooi het inloggen in je browser.",
@@ -32,8 +29,7 @@
"delete": "Verwijderen",
"shuffle": "Willekeurig",
"addTo": "Toevoegen aan...",
"remove": "Verwijderen",
"paste": "Plakken",
"createNew": "Nieuw aanmaken",
"connect": "Verbinden",
"disconnect": "Verbinding verbreken",
"play": "Afspelen",
@@ -43,10 +39,9 @@
"search": "Zoeken",
"home": "Home",
"back": "Terug",
"settings": "Instellingen",
"settings": "Opties",
"mute": "Dempen",
"ok": "OK",
"loading": "Laden...",
"reconnect": "Opnieuw verbinden",
"exitConfirmTitle": "App afsluiten?",
"exitConfirmMessage": "Weet je zeker dat je wilt afsluiten?",
@@ -57,13 +52,14 @@
"refreshingServers": "Servers vernieuwen...",
"loadingServers": "Servers laden...",
"connectingToServers": "Verbinden met servers...",
"startingOfflineMode": "Offlinemodus starten..."
"startingOfflineMode": "Offlinemodus starten...",
"loading": "Laden..."
},
"screens": {
"licenses": "Licenties",
"switchProfile": "Wissel van profiel",
"subtitleStyling": "Ondertitel opmaak",
"mpvConfig": "MPV-configuratie",
"mpvConfig": "mpv.conf",
"logs": "Logbestanden"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Instellingen",
"supportDeveloper": "Steun Plezy",
"supportDeveloperDescription": "Doneer via Liberapay om de ontwikkeling te steunen",
"language": "Taal",
"theme": "Thema",
"appearance": "Uiterlijk",
"videoPlayback": "Video afspelen",
"videoPlaybackDescription": "Afspeelgedrag configureren",
"advanced": "Geavanceerd",
"episodePosterMode": "Aflevering poster stijl",
"seriesPoster": "Serie poster",
"seriesPosterDescription": "Toon de serie poster voor alle afleveringen",
"seasonPoster": "Seizoen poster",
"seasonPosterDescription": "Toon de seizoensspecifieke poster voor afleveringen",
"episodeThumbnail": "Aflevering miniatuur",
"episodeThumbnailDescription": "Toon 16:9 aflevering miniaturen",
"episodeThumbnail": "Miniatuur",
"showHeroSectionDescription": "Toon uitgelichte inhoud carrousel op startscherm",
"secondsLabel": "Seconden",
"minutesLabel": "Minuten",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Voer duur in (${min}-${max})",
"systemTheme": "Systeem",
"systemThemeDescription": "Volg systeeminstellingen",
"lightTheme": "Licht",
"darkTheme": "Donker",
"oledTheme": "OLED",
"oledThemeDescription": "Puur zwart voor OLED-schermen",
"libraryDensity": "Bibliotheek dichtheid",
"compact": "Compact",
"compactDescription": "Kleinere kaarten, meer items zichtbaar",
"normal": "Normaal",
"normalDescription": "Standaard grootte",
"comfortable": "Comfortabel",
"comfortableDescription": "Grotere kaarten, minder items zichtbaar",
"viewMode": "Weergavemodus",
"gridView": "Raster",
"gridViewDescription": "Items weergeven in een rasterindeling",
"listView": "Lijst",
"listViewDescription": "Items weergeven in een lijstindeling",
"showHeroSection": "Toon hoofdsectie",
"useGlobalHubs": "Plex Home-indeling gebruiken",
"useGlobalHubsDescription": "Toon startpagina-hubs zoals de officiële Plex-client. Indien uitgeschakeld, worden in plaats daarvan aanbevelingen per bibliotheek getoond.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Miniaturen vervagen en beschrijvingen verbergen voor afleveringen die je nog niet hebt gezien",
"playerBackend": "Speler backend",
"exoPlayer": "ExoPlayer (Aanbevolen)",
"exoPlayerDescription": "Android-native speler met betere hardware-ondersteuning",
"mpv": "MPV",
"mpvDescription": "Geavanceerde speler met meer functies en ASS-ondertitelondersteuning",
"mpv": "mpv",
"hardwareDecoding": "Hardware decodering",
"hardwareDecodingDescription": "Gebruik hardware versnelling indien beschikbaar",
"bufferSize": "Buffer grootte",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Pas ondertitel uiterlijk aan",
"smallSkipDuration": "Korte skip duur",
"largeSkipDuration": "Lange skip duur",
"rewindOnResume": "Terugspoelen bij hervatten",
"secondsUnit": "${seconds} seconden",
"defaultSleepTimer": "Standaard slaap timer",
"minutesUnit": "bij ${minutes} minuten",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Pas toetsenbord sneltoetsen aan",
"videoPlayerNavigation": "Videospeler navigatie",
"videoPlayerNavigationDescription": "Gebruik pijltjestoetsen om door de videospeler bediening te navigeren",
"watchTogetherRelay": "Samen Kijken Relay",
"watchTogetherRelayDescription": "Stel een aangepaste relay-server in voor Samen Kijken. Alle deelnemers moeten dezelfde server gebruiken.",
"watchTogetherRelayHint": "https://mijn-relay.voorbeeld.nl",
"crashReporting": "Crashrapportage",
"crashReportingDescription": "Crashrapporten verzenden om de app te verbeteren",
"debugLogging": "Debug logging",
"debugLoggingDescription": "Schakel gedetailleerde logging in voor probleemoplossing",
"viewLogs": "Bekijk logs",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Credits automatisch overslaan en volgende aflevering afspelen",
"autoSkipDelay": "Vertraging Automatisch Overslaan",
"autoSkipDelayDescription": "${seconds} seconden wachten voor automatisch overslaan",
"introPattern": "Intromarkeringspatroon",
"introPatternDescription": "Reguliere expressie om intromarkeringen in hoofdstuktitels te herkennen",
"creditsPattern": "Aftitelingmarkeringspatroon",
"creditsPatternDescription": "Reguliere expressie om aftitelingmarkeringen in hoofdstuktitels te herkennen",
"invalidRegex": "Ongeldige reguliere expressie",
"downloads": "Downloads",
"downloadLocationDescription": "Kies waar gedownloade content wordt opgeslagen",
"downloadLocationDefault": "Standaard (App-opslag)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Kan map niet selecteren",
"downloadOnWifiOnly": "Alleen via WiFi downloaden",
"downloadOnWifiOnlyDescription": "Voorkom downloads bij gebruik van mobiele data",
"autoRemoveWatchedDownloads": "Bekeken downloads automatisch verwijderen",
"autoRemoveWatchedDownloadsDescription": "Gedownloade afleveringen en films automatisch verwijderen wanneer ze als bekeken zijn gemarkeerd",
"cellularDownloadBlocked": "Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.",
"maxVolume": "Maximaal volume",
"maxVolumeDescription": "Volume boven 100% toestaan voor stille media",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Toon op Discord wat je aan het kijken bent",
"trakt": "Trakt",
"traktDescription": "Kijkgeschiedenis synchroniseren met Trakt",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Sta mobiele apparaten op je netwerk toe om deze app te bedienen",
"autoPip": "Automatische beeld-in-beeld",
"autoPipDescription": "Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen",
"matchContentFrameRate": "Inhoudsframesnelheid afstemmen",
"matchContentFrameRateDescription": "Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Verversingssnelheid afstemmen",
"matchRefreshRateDescription": "Schermverversingssnelheid aanpassen aan de video-inhoud op volledig scherm",
"matchDynamicRange": "Dynamisch bereik afstemmen",
"matchDynamicRangeDescription": "HDR automatisch inschakelen voor HDR-inhoud en terugkeren naar SDR bij het verlaten van de speler",
"displaySwitchDelay": "Vertraging bij schermwisseling",
"tunneledPlayback": "Getunnelde weergave",
"tunneledPlaybackDescription": "Gebruik hardwareversnelde videotunneling. Schakel uit als je een zwart scherm met geluid ziet bij HDR-content",
"requireProfileSelectionOnOpen": "Vraag om profiel bij openen",
"requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend",
"confirmExitOnBack": "Bevestigen voor afsluiten",
"confirmExitOnBackDescription": "Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten"
"confirmExitOnBackDescription": "Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten",
"forceTvMode": "TV-modus forceren",
"forceTvModeDescription": "Gebruik de TV-indeling ongeacht automatische detectie. Handig op Android TV-apparaten die de leanback-functie niet melden. Start de app opnieuw bij wijziging.",
"autoHidePerformanceOverlay": "Prestatie-overlay automatisch verbergen",
"autoHidePerformanceOverlayDescription": "Laat de prestatie-overlay meevervagen met de afspeelknoppen",
"showNavBarLabels": "Navigatiebalk labels tonen",
"showNavBarLabelsDescription": "Tekstlabels onder de pictogrammen van de navigatiebalk weergeven",
"liveTvDefaultFavorites": "Standaard favoriete zenders",
"liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Zoek films, series, muziek...",
@@ -239,11 +263,6 @@
"skipMarker": "Intro/aftiteling overslaan"
}
},
"pinEntry": {
"enterPin": "Voer PIN in",
"showPin": "Toon PIN",
"hidePin": "Verberg PIN"
},
"fileInfo": {
"title": "Bestand info",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Kleurprimaires",
"chromaSubsampling": "Chroma subsampling",
"channels": "Kanalen",
"subtitles": "Ondertitels",
"overallBitrate": "Totale bitrate",
"path": "Pad",
"size": "Grootte",
"container": "Container",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Dit omvat alle afleveringen en hun bestanden.",
"mediaDeletedSuccessfully": "Media-item succesvol verwijderd",
"mediaFailedToDelete": "Verwijderen van media-item mislukt",
"rate": "Beoordelen"
"rate": "Beoordelen",
"playFromBeginning": "Afspelen vanaf het begin",
"playVersion": "Versie afspelen..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
@@ -316,7 +339,6 @@
"unlockRotation": "Ontgrendel rotatie",
"timerActive": "Timer actief",
"playbackWillPauseIn": "Afspelen wordt gepauzeerd over ${duration}",
"sleepTimerCompleted": "Slaaptimer voltooid - afspelen gepauzeerd",
"stillWatching": "Kijk je nog?",
"pausingIn": "Pauze over ${seconds}s",
"continueWatching": "Doorgaan",
@@ -333,34 +355,43 @@
"muteButton": "Dempen",
"unmuteButton": "Dempen opheffen",
"settingsButton": "Video-instellingen",
"audioTrackButton": "Audiosporen",
"subtitlesButton": "Ondertitels",
"tracksButton": "Audio en ondertitels",
"chaptersButton": "Hoofdstukken",
"versionsButton": "Videoversies",
"pipButton": "Beeld-in-beeld modus",
"aspectRatioButton": "Beeldverhouding",
"ambientLighting": "Omgevingsverlichting",
"ambientLightingOn": "Omgevingsverlichting inschakelen",
"ambientLightingOff": "Omgevingsverlichting uitschakelen",
"fullscreenButton": "Volledig scherm activeren",
"exitFullscreenButton": "Volledig scherm verlaten",
"alwaysOnTopButton": "Altijd bovenop",
"rotationLockButton": "Rotatievergrendeling",
"lockScreen": "Vergrendel scherm",
"screenLockButton": "Schermvergrendeling",
"longPressToUnlock": "Lang indrukken om te ontgrendelen",
"timelineSlider": "Videotijdlijn",
"volumeSlider": "Volumeniveau",
"endsAt": "Eindigt om ${time}",
"pipActive": "Afspelen in beeld-in-beeld",
"pipFailed": "Beeld-in-beeld kon niet worden gestart",
"pipErrors": {
"androidVersion": "Vereist Android 8.0 of nieuwer",
"iosVersion": "Vereist iOS 15.0 of nieuwer",
"permissionDisabled": "Beeld-in-beeld toestemming is uitgeschakeld. Schakel deze in via Instellingen > Apps > Plezy > Beeld-in-beeld",
"notSupported": "Dit apparaat ondersteunt geen beeld-in-beeld modus",
"voSwitchFailed": "Kan video-uitvoer niet wisselen voor beeld-in-beeld",
"failed": "Beeld-in-beeld kon niet worden gestart",
"unknown": "Er is een fout opgetreden: ${error}"
},
"chapters": "Hoofdstukken",
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "Wachtrij",
"noQueueItems": "Geen items in de wachtrij",
"searchSubtitles": "Ondertitels zoeken",
"language": "Taal",
"noSubtitlesFound": "Geen ondertitels gevonden",
"subtitleDownloaded": "Ondertitel gedownload",
"subtitleDownloadFailed": "Ondertitel downloaden mislukt",
"searchLanguages": "Talen zoeken..."
},
"userStatus": {
"admin": "Beheerder",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Gemarkeerd als ongekeken",
"markedAsWatchedOffline": "Gemarkeerd als gekeken (sync wanneer online)",
"markedAsUnwatchedOffline": "Gemarkeerd als ongekeken (sync wanneer online)",
"autoRemovedWatchedDownload": "Automatisch verwijderd: ${title}",
"removedFromContinueWatching": "Verwijderd uit Doorgaan met kijken",
"errorLoading": "Fout: ${error}",
"fileInfoNotAvailable": "Bestand informatie niet beschikbaar",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Kan afspeelwachtrij niet maken - geen items",
"failedPlayback": "Afspelen van ${action} mislukt: ${error}",
"switchingToCompatiblePlayer": "Overschakelen naar compatibele speler...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Afspelen mislukt",
"serverLimitBody": "De server gaf een fout terug (HTTP 500). Dit betekent meestal dat de servereigenaar een bandbreedte- of transcoderingslimiet heeft ingesteld die jouw sessie weigert. Er valt vanaf de client niets te doen — de servereigenaar moet de instellingen aanpassen.",
"logsUploaded": "Logs geüpload",
"logsUploadFailed": "Uploaden van logs mislukt",
"logId": "Log-ID"
},
"subtitlingStyling": {
"stylingOptions": "Opmaak opties",
"text": "Tekst",
"border": "Rand",
"background": "Achtergrond",
"fontSize": "Lettergrootte",
"textColor": "Tekstkleur",
"borderSize": "Rand grootte",
"borderColor": "Randkleur",
"backgroundOpacity": "Achtergrond transparantie",
"backgroundColor": "Achtergrondkleur",
"position": "Position"
"position": "Positie",
"assOverride": "ASS-overschrijving",
"bold": "Vet",
"italic": "Cursief"
},
"mpvConfig": {
"title": "MPV-configuratie",
"title": "mpv-configuratie",
"description": "Geavanceerde videospeler-instellingen",
"properties": "Eigenschappen",
"presets": "Voorinstellingen",
"noProperties": "Geen eigenschappen geconfigureerd",
"noPresets": "Geen opgeslagen voorinstellingen",
"addProperty": "Eigenschap toevoegen",
"editProperty": "Eigenschap bewerken",
"deleteProperty": "Eigenschap verwijderen",
"propertyKey": "Eigenschapssleutel",
"propertyKeyHint": "bijv. hwdec, demuxer-max-bytes",
"propertyValue": "Eigenschapswaarde",
"propertyValueHint": "bijv. auto, 256000000",
"saveAsPreset": "Opslaan als voorinstelling...",
"presetName": "Naam voorinstelling",
"presetNameHint": "Voer een naam in voor deze voorinstelling",
@@ -436,8 +466,7 @@
"presetLoaded": "Voorinstelling geladen",
"presetDeleted": "Voorinstelling verwijderd",
"confirmDeletePreset": "Weet je zeker dat je deze voorinstelling wilt verwijderen?",
"confirmDeleteProperty": "Weet je zeker dat je deze eigenschap wilt verwijderen?",
"entriesCount": "${count} items"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Bevestig actie"
@@ -452,11 +481,8 @@
"overview": "Overzicht",
"cast": "Acteurs",
"extras": "Trailers & Extra's",
"seasons": "Seizoenen",
"studio": "Studio",
"rating": "Leeftijd",
"episodeCount": "${count} afleveringen",
"watchedProgress": "${watched}/${total} gekeken",
"movie": "Film",
"tvShow": "TV Serie",
"minutesLeft": "${minutes} min over"
@@ -518,6 +544,7 @@
"playlists": "Afspeellijsten"
},
"groupings": {
"title": "Groepering",
"all": "Alles",
"movies": "Films",
"shows": "Series",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Wis logs",
"copyLogs": "Kopieer logs",
"uploadLogs": "Upload Logs",
"error": "Fout:",
"stackTrace": "Stacktracering:"
"uploadLogs": "Logs uploaden"
},
"licenses": {
"relatedPackages": "Gerelateerde pakketten",
@@ -559,26 +584,18 @@
"licensesCount": "${count} licenties"
},
"navigation": {
"libraries": "Bibliotheken",
"libraries": "Media",
"downloads": "Downloads",
"liveTv": "Live TV"
},
"liveTv": {
"title": "Live TV",
"channels": "Zenders",
"guide": "Gids",
"noChannels": "Geen zenders beschikbaar",
"noDvr": "Geen DVR geconfigureerd op een server",
"tuneFailed": "Kan zender niet afstemmen",
"loading": "Zenders laden...",
"nowPlaying": "Nu aan het afspelen",
"noPrograms": "Geen programmagegevens beschikbaar",
"channelNumber": "Kanaal ${number}",
"live": "LIVE",
"hd": "HD",
"premiere": "NIEUW",
"reloadGuide": "Gids herladen",
"allChannels": "Alle zenders",
"now": "Nu",
"today": "Vandaag",
"midnight": "Middernacht",
@@ -588,30 +605,41 @@
"evening": "Avond",
"lateNight": "Late avond",
"whatsOn": "Nu op TV",
"watchChannel": "Kanaal bekijken"
"watchChannel": "Kanaal bekijken",
"favorites": "Favorieten",
"reorderFavorites": "Favorieten herordenen",
"joinSession": "Deelnemen aan lopende sessie",
"watchFromStart": "Kijk vanaf het begin (${minutes} min geleden)",
"watchLive": "Live kijken",
"goToLive": "Ga naar live"
},
"downloads": {
"title": "Downloads",
"manage": "Beheren",
"tvShows": "Series",
"movies": "Films",
"noDownloads": "Nog geen downloads",
"noDownloadsDescription": "Gedownloade content verschijnt hier voor offline weergave",
"downloadNow": "Download",
"deleteDownload": "Download verwijderen",
"retryDownload": "Download opnieuw proberen",
"downloadQueued": "Download in wachtrij",
"episodesQueued": "${count} afleveringen in wachtrij voor download",
"downloadDeleted": "Download verwijderd",
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.",
"deletingWithProgress": "Verwijderen van ${title}... (${current} van ${total})",
"noDownloadsTree": "Geen downloads",
"pauseAll": "Alles pauzeren",
"resumeAll": "Alles hervatten",
"deleteAll": "Alles verwijderen"
"collections": {
"title": "Collecties",
"collection": "Collectie",
"empty": "Collectie is leeg",
"unknownLibrarySection": "Kan niet verwijderen: onbekende bibliotheeksectie",
"deleteCollection": "Collectie verwijderen",
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"deleted": "Collectie verwijderd",
"deleteFailed": "Collectie verwijderen mislukt",
"deleteFailedWithError": "Collectie verwijderen mislukt: ${error}",
"failedToLoadItems": "Collectie-items laden mislukt: ${error}",
"selectCollection": "Selecteer collectie",
"collectionName": "Collectienaam",
"enterCollectionName": "Voer collectienaam in",
"addedToCollection": "Toegevoegd aan collectie",
"errorAddingToCollection": "Fout bij toevoegen aan collectie",
"created": "Collectie gemaakt",
"removeFromCollection": "Verwijderen uit collectie",
"removeFromCollectionConfirm": "\"${title}\" uit deze collectie verwijderen?",
"removedFromCollection": "Uit collectie verwijderd",
"removeFromCollectionFailed": "Verwijderen uit collectie mislukt",
"removeFromCollectionError": "Fout bij verwijderen uit collectie: ${error}",
"searchCollections": "Collecties zoeken..."
},
"playlists": {
"title": "Afspeellijsten",
"playlist": "Afspeellijst",
"noPlaylists": "Geen afspeellijsten gevonden",
"create": "Afspeellijst maken",
"playlistName": "Naam afspeellijst",
@@ -629,38 +657,12 @@
"itemAdded": "Toegevoegd aan afspeellijst",
"itemRemoved": "Verwijderd uit afspeellijst",
"selectPlaylist": "Selecteer afspeellijst",
"createNewPlaylist": "Nieuwe afspeellijst maken",
"errorCreating": "Fout bij maken afspeellijst",
"errorDeleting": "Fout bij verwijderen afspeellijst",
"errorLoading": "Fout bij laden afspeellijsten",
"errorAdding": "Fout bij toevoegen aan afspeellijst",
"errorReordering": "Fout bij herschikken van afspeellijstitem",
"errorRemoving": "Fout bij verwijderen uit afspeellijst",
"playlist": "Afspeellijst"
},
"collections": {
"title": "Collecties",
"collection": "Collectie",
"empty": "Collectie is leeg",
"unknownLibrarySection": "Kan niet verwijderen: onbekende bibliotheeksectie",
"deleteCollection": "Collectie verwijderen",
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"deleted": "Collectie verwijderd",
"deleteFailed": "Collectie verwijderen mislukt",
"deleteFailedWithError": "Collectie verwijderen mislukt: ${error}",
"failedToLoadItems": "Collectie-items laden mislukt: ${error}",
"selectCollection": "Selecteer collectie",
"createNewCollection": "Nieuwe collectie maken",
"collectionName": "Collectienaam",
"enterCollectionName": "Voer collectienaam in",
"addedToCollection": "Toegevoegd aan collectie",
"errorAddingToCollection": "Fout bij toevoegen aan collectie",
"created": "Collectie gemaakt",
"removeFromCollection": "Verwijderen uit collectie",
"removeFromCollectionConfirm": "\"${title}\" uit deze collectie verwijderen?",
"removedFromCollection": "Uit collectie verwijderd",
"removeFromCollectionFailed": "Verwijderen uit collectie mislukt",
"removeFromCollectionError": "Fout bij verwijderen uit collectie: ${error}"
"errorRemoving": "Fout bij verwijderen uit afspeellijst"
},
"watchTogether": {
"title": "Samen Kijken",
@@ -697,18 +699,76 @@
"leave": "Verlaten",
"syncing": "Synchroniseren...",
"joinWatchSession": "Kijksessie Deelnemen",
"enterCodeHint": "Voer 8-teken code in",
"enterCodeHint": "Voer 5-teken code in",
"pasteFromClipboard": "Plakken van klembord",
"pleaseEnterCode": "Voer een sessiecode in",
"codeMustBe8Chars": "Sessiecode moet 8 tekens zijn",
"codeMustBe5Chars": "Sessiecode moet 5 tekens zijn",
"joinInstructions": "Voer de sessiecode in die door de host is gedeeld om deel te nemen aan hun kijksessie.",
"failedToCreate": "Sessie maken mislukt",
"failedToJoin": "Sessie deelnemen mislukt",
"sessionCodeCopied": "Sessiecode gekopieerd naar klembord",
"relayUnreachable": "De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.",
"reconnectingToHost": "Opnieuw verbinden met host...",
"currentPlayback": "Huidige weergave",
"joinCurrentPlayback": "Deelnemen aan huidige weergave",
"joinCurrentPlaybackDescription": "Ga terug naar wat de host nu kijkt",
"failedToOpenCurrentPlayback": "Huidige weergave kon niet worden geopend",
"participantJoined": "${name} is toegetreden",
"participantLeft": "${name} heeft de sessie verlaten"
"participantLeft": "${name} heeft de sessie verlaten",
"participantPaused": "${name} heeft gepauzeerd",
"participantResumed": "${name} heeft hervat",
"participantSeeked": "${name} heeft gespoeld",
"participantBuffering": "${name} is aan het bufferen",
"waitingForParticipants": "Wachten tot anderen geladen zijn...",
"recentRooms": "Recente kamers",
"renameRoom": "Kamer hernoemen",
"removeRoom": "Verwijderen"
},
"downloads": {
"title": "Downloads",
"manage": "Beheren",
"tvShows": "Series",
"movies": "Films",
"noDownloads": "Nog geen downloads",
"noDownloadsDescription": "Gedownloade content verschijnt hier voor offline weergave",
"downloadNow": "Download",
"deleteDownload": "Download verwijderen",
"retryDownload": "Download opnieuw proberen",
"downloadQueued": "Download in wachtrij",
"serverErrorBitrate": "Serverfout — het bestand overschrijdt mogelijk de bitrate-limiet voor remote streaming",
"episodesQueued": "${count} afleveringen in wachtrij voor download",
"downloadDeleted": "Download verwijderd",
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.",
"deletingWithProgress": "Verwijderen van ${title}... (${current} van ${total})",
"noDownloadsTree": "Geen downloads",
"pauseAll": "Alles pauzeren",
"resumeAll": "Alles hervatten",
"deleteAll": "Alles verwijderen",
"selectVersion": "Versie selecteren",
"allEpisodes": "Alle afleveringen",
"unwatchedOnly": "Alleen onbekeken",
"nextNUnwatched": "Volgende ${count} onbekeken",
"customAmount": "Aangepast aantal...",
"howManyEpisodes": "Hoeveel afleveringen?",
"itemsQueued": "${count} items in downloadwachtrij",
"keepSynced": "Gesynchroniseerd houden",
"downloadOnce": "Eenmalig downloaden",
"keepNUnwatched": "${count} onbekeken behouden",
"editSyncRule": "Synchronisatieregel bewerken",
"removeSyncRule": "Synchronisatieregel verwijderen",
"removeSyncRuleConfirm": "Synchronisatie van \"${title}\" stoppen? Gedownloade afleveringen worden behouden.",
"syncRuleCreated": "Synchronisatieregel aangemaakt — ${count} onbekeken afleveringen behouden",
"syncRuleUpdated": "Synchronisatieregel bijgewerkt",
"syncRuleRemoved": "Synchronisatieregel verwijderd",
"syncedNewEpisodes": "${count} nieuwe afleveringen gesynchroniseerd voor ${title}",
"activeSyncRules": "Synchronisatieregels",
"noSyncRules": "Geen synchronisatieregels",
"manageSyncRule": "Synchronisatie beheren",
"editEpisodeCount": "Aantal afleveringen",
"editSyncFilter": "Synchronisatiefilter",
"syncAllItems": "Alle items synchroniseren",
"syncUnwatchedItems": "Ongekeken items synchroniseren",
"syncRuleListCreated": "Synchronisatieregel aangemaakt"
},
"shaders": {
"title": "Shaders",
@@ -716,67 +776,53 @@
"nvscalerDescription": "NVIDIA-beeldschaling voor scherpere video",
"qualityFast": "Snel",
"qualityHQ": "Hoge kwaliteit",
"mode": "Modus"
"mode": "Modus",
"importShader": "Shader importeren",
"customShaderDescription": "Aangepaste GLSL-shader",
"shaderImported": "Shader geïmporteerd",
"shaderImportFailed": "Shader importeren mislukt",
"deleteShader": "Shader verwijderen",
"deleteShaderConfirm": "\"${name}\" verwijderen?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Verbinden met apparaat",
"hostRemoteSession": "Externe sessie hosten",
"controlThisDevice": "Bedien dit apparaat met je telefoon",
"remoteControl": "Afstandsbediening",
"controlDesktop": "Bedien een desktop-apparaat",
"title": "Afstandsbediening",
"connectedTo": "Verbonden met ${name}",
"session": {
"creatingSession": "Externe sessie aanmaken...",
"failedToCreate": "Kan externe sessie niet aanmaken:",
"noSession": "Geen sessie beschikbaar",
"scanQrCode": "Scan QR-code",
"orEnterManually": "Of voer handmatig in",
"startingServer": "Externe server starten...",
"failedToCreate": "Kan externe server niet starten:",
"hostAddress": "Hostadres",
"sessionId": "Sessie-ID",
"pin": "PIN",
"connected": "Verbonden",
"waitingForConnection": "Wachten op verbinding...",
"serverRunning": "Externe server actief",
"serverStopped": "Externe server gestopt",
"serverRunningDescription": "Mobiele apparaten op je netwerk kunnen deze app ontdekken en ermee verbinden",
"serverStoppedDescription": "Start de server om mobiele apparaten te laten verbinden",
"usePhoneToControl": "Gebruik je mobiele apparaat om deze app te bedienen",
"copiedToClipboard": "${label} gekopieerd naar klembord",
"copyToClipboard": "Kopieer naar klembord",
"newSession": "Nieuwe sessie",
"startServer": "Server starten",
"stopServer": "Server stoppen",
"minimize": "Minimaliseren"
},
"pairing": {
"scan": "Scannen",
"manual": "Handmatig",
"pairWithDesktop": "Koppelen met desktop",
"enterSessionDetails": "Voer de sessiegegevens in die op je desktop-apparaat worden getoond",
"discoveryDescription": "Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Voer 8-tekens sessie-ID in",
"pinHint": "Voer 6-cijferige PIN in",
"connecting": "Verbinden...",
"tips": "Tips",
"tipDesktop": "Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu",
"tipScan": "Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen",
"tipWifi": "Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten",
"cameraPermissionRequired": "Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.",
"cameraError": "Kan camera niet starten: ${error}",
"scanInstruction": "Richt je camera op de QR-code die op je desktop wordt getoond",
"invalidQrCode": "Ongeldig QR-codeformaat",
"validationHostRequired": "Voer een hostadres in",
"searchingForDevices": "Apparaten zoeken...",
"noDevicesFound": "Geen apparaten gevonden op je netwerk",
"noDevicesHint": "Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten",
"availableDevices": "Beschikbare apparaten",
"manualConnection": "Handmatige verbinding",
"cryptoInitFailed": "Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.",
"validationHostRequired": "Voer het hostadres in",
"validationHostFormat": "Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)",
"validationSessionIdRequired": "Voer een sessie-ID in",
"validationSessionIdLength": "Sessie-ID moet 8 tekens zijn",
"validationPinRequired": "Voer een PIN in",
"validationPinLength": "PIN moet 6 cijfers zijn",
"connectionTimedOut": "Verbinding verlopen. Controleer de sessie-ID en PIN.",
"sessionNotFound": "Kan de sessie niet vinden. Controleer je gegevens.",
"failedToConnect": "Verbinden mislukt: ${error}"
"connectionTimedOut": "Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.",
"sessionNotFound": "Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.",
"authFailed": "Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.",
"failedToConnect": "Kan niet verbinden: ${error}"
},
"remote": {
"disconnectConfirm": "Wil je de verbinding met de externe sessie verbreken?",
"reconnecting": "Opnieuw verbinden...",
"attemptOf": "Poging ${current} van 5",
"retryNow": "Nu opnieuw proberen",
"connectionError": "Verbindingsfout",
"notConnected": "Niet verbonden",
"tabRemote": "Afstandsbediening",
"tabPlay": "Afspelen",
"tabMore": "Meer",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Externe speler gebruiken",
"useExternalPlayerDescription": "Open video's in een externe app in plaats van de ingebouwde speler",
"selectPlayer": "Speler selecteren",
"customPlayers": "Aangepaste spelers",
"systemDefault": "Systeemstandaard",
"addCustomPlayer": "Aangepaste speler toevoegen",
"playerName": "Spelernaam",
"playerCommand": "Commando",
"playerPackage": "Pakketnaam",
"playerUrlScheme": "URL-schema",
"customPlayer": "Aangepaste speler",
"off": "Uit",
"launchFailed": "Kan externe speler niet openen",
"appNotInstalled": "${name} is niet geïnstalleerd",
@@ -847,8 +893,12 @@
"summary": "Samenvatting",
"poster": "Poster",
"background": "Achtergrond",
"logo": "Logo",
"squareArt": "Vierkante afbeelding",
"selectPoster": "Poster selecteren",
"selectBackground": "Achtergrond selecteren",
"selectLogo": "Logo selecteren",
"selectSquareArt": "Vierkante afbeelding selecteren",
"fromUrl": "Vanaf URL",
"uploadFile": "Bestand uploaden",
"enterImageUrl": "Voer afbeeldings-URL in",
@@ -890,6 +940,39 @@
"subtitleMode": "Automatische ondertitelselectie",
"manuallySelected": "Handmatig geselecteerd",
"shownWithForeignAudio": "Weergeven bij anderstalig geluid",
"alwaysEnabled": "Altijd ingeschakeld"
"alwaysEnabled": "Altijd ingeschakeld",
"tags": "Tags",
"addTag": "Tag toevoegen",
"genre": "Genre",
"director": "Regisseur",
"writer": "Schrijver",
"producer": "Producent",
"country": "Land",
"collection": "Collectie",
"label": "Label",
"style": "Stijl",
"mood": "Stemming"
},
"serverTasks": {
"title": "Servertaken",
"failedToLoad": "Taken konden niet worden geladen",
"noTasks": "Geen actieve taken"
},
"trakt": {
"title": "Trakt",
"connected": "Verbonden",
"connectedAs": "Verbonden als @${username}",
"disconnectConfirm": "Trakt-account loskoppelen?",
"disconnectConfirmBody": "Plezy stopt met het versturen van afspeelgebeurtenissen naar Trakt. Je kunt op elk moment opnieuw verbinden.",
"scrobble": "Realtime scrobbling",
"scrobbleDescription": "Verstuur play-, pauze- en stopgebeurtenissen tijdens afspelen naar Trakt.",
"watchedSync": "Bekeken-status synchroniseren",
"watchedSyncDescription": "Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd.",
"deviceCodeTitle": "Activeer Plezy op Trakt",
"deviceCodeBody": "Ga naar ${url} en voer deze code in:",
"openTraktActivate": "Open Trakt om te activeren",
"waitingForAuthorization": "Wacht op autorisatie…",
"codeCopied": "Code gekopieerd",
"connectFailed": "Kan niet verbinden met Trakt. Probeer opnieuw."
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Zaloguj się przez Plex",
"showQRCode": "Pokaż kod QR",
"authenticate": "Uwierzytelnienie",
"authenticationTimeout": "Upłynął czas uwierzytelniania. Spróbuj ponownie.",
"scanQRToSignIn": "Zeskanuj ten kod QR, aby się zalogować",
"waitingForAuth": "Oczekiwanie na uwierzytelnienie...\nDokończ logowanie w przeglądarce.",
"useBrowser": "Użyj przeglądarki"
},
"common": {
"cancel": "Anuluj",
"save": "Zapisz",
"close": "Zamknij",
"clear": "Wyczyść",
"reset": "Resetuj",
"later": "Później",
"submit": "Wyślij",
"confirm": "Potwierdź",
"retry": "Ponów",
"logout": "Wyloguj",
"unknown": "Nieznane",
"refresh": "Odśwież",
"yes": "Tak",
"no": "Nie",
"delete": "Usuń",
"shuffle": "Losowo",
"addTo": "Dodaj do...",
"createNew": "Utwórz nowy",
"connect": "Połącz",
"disconnect": "Rozłącz",
"play": "Odtwórz",
"pause": "Pauza",
"resume": "Wznów",
"error": "Błąd",
"search": "Szukaj",
"home": "Strona główna",
"back": "Wstecz",
"settings": "Ustawienia",
"mute": "Wycisz",
"ok": "OK",
"reconnect": "Połącz ponownie",
"exitConfirmTitle": "Zamknąć aplikację?",
"exitConfirmMessage": "Czy na pewno chcesz wyjść?",
"dontAskAgain": "Nie pytaj ponownie",
"exit": "Wyjdź",
"viewAll": "Pokaż wszystko",
"checkingNetwork": "Sprawdzanie sieci...",
"refreshingServers": "Odświeżanie serwerów...",
"loadingServers": "Ładowanie serwerów...",
"connectingToServers": "Łączenie z serwerami...",
"startingOfflineMode": "Uruchamianie trybu offline...",
"loading": "Ładowanie..."
},
"screens": {
"licenses": "Licencje",
"switchProfile": "Zmień profil",
"subtitleStyling": "Styl napisów",
"mpvConfig": "mpv.conf",
"logs": "Logi"
},
"update": {
"available": "Dostępna aktualizacja",
"versionAvailable": "Dostępna wersja ${version}",
"currentVersion": "Bieżąca: ${version}",
"skipVersion": "Pomiń tę wersję",
"viewRelease": "Zobacz wydanie",
"latestVersion": "Masz najnowszą wersję",
"checkFailed": "Nie udało się sprawdzić aktualizacji"
},
"settings": {
"title": "Ustawienia",
"supportDeveloper": "Wesprzyj Plezy",
"supportDeveloperDescription": "Wspomóż rozwój darowizną na Liberapay",
"language": "Język",
"theme": "Motyw",
"appearance": "Wygląd",
"videoPlayback": "Odtwarzanie wideo",
"videoPlaybackDescription": "Skonfiguruj zachowanie odtwarzania",
"advanced": "Zaawansowane",
"episodePosterMode": "Styl plakatu odcinka",
"seriesPoster": "Plakat serialu",
"seasonPoster": "Plakat sezonu",
"episodeThumbnail": "Miniatura",
"showHeroSectionDescription": "Wyświetl karuzelę wyróżnionych treści na ekranie głównym",
"secondsLabel": "Sekundy",
"minutesLabel": "Minuty",
"secondsShort": "s",
"minutesShort": "m",
"durationHint": "Wprowadź czas (${min}-${max})",
"systemTheme": "Systemowy",
"lightTheme": "Jasny",
"darkTheme": "Ciemny",
"oledTheme": "OLED",
"libraryDensity": "Gęstość biblioteki",
"compact": "Kompaktowy",
"comfortable": "Wygodny",
"viewMode": "Tryb widoku",
"gridView": "Siatka",
"listView": "Lista",
"showHeroSection": "Pokaż sekcję wyróżnioną",
"useGlobalHubs": "Użyj układu Plex Home",
"useGlobalHubsDescription": "Pokaż huby strony głównej jak w oficjalnym kliencie Plex. Gdy wyłączone, pokazuje rekomendacje per biblioteka.",
"showServerNameOnHubs": "Pokaż nazwę serwera w hubach",
"showServerNameOnHubsDescription": "Zawsze wyświetlaj nazwę serwera w tytułach hubów. Gdy wyłączone, pokazuje tylko dla zduplikowanych nazw.",
"alwaysKeepSidebarOpen": "Zawsze utrzymuj panel boczny otwarty",
"alwaysKeepSidebarOpenDescription": "Panel boczny jest rozwinięty, a obszar treści dostosowuje się",
"showUnwatchedCount": "Pokaż liczbę nieobejrzanych",
"showUnwatchedCountDescription": "Wyświetl liczbę nieobejrzanych odcinków w serialach i sezonach",
"hideSpoilers": "Ukryj spoilery nieobejrzanych odcinków",
"hideSpoilersDescription": "Rozmyj miniatury i ukryj opisy odcinków, których jeszcze nie obejrzałeś",
"playerBackend": "Backend odtwarzacza",
"exoPlayer": "ExoPlayer (Zalecany)",
"mpv": "mpv",
"hardwareDecoding": "Dekodowanie sprzętowe",
"hardwareDecodingDescription": "Użyj akceleracji sprzętowej, gdy dostępna",
"bufferSize": "Rozmiar bufora",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automatyczny (Zalecany)",
"bufferSizeWarning": "Twoje urządzenie ma ${heap}MB pamięci. Bufor ${size}MB może powodować problemy z odtwarzaniem.",
"subtitleStyling": "Styl napisów",
"subtitleStylingDescription": "Dostosuj wygląd napisów",
"smallSkipDuration": "Krótki skok",
"largeSkipDuration": "Długi skok",
"rewindOnResume": "Przewiń przy wznowieniu",
"secondsUnit": "${seconds} sekund",
"defaultSleepTimer": "Domyślny wyłącznik czasowy",
"minutesUnit": "${minutes} minut",
"rememberTrackSelections": "Zapamiętaj wybór ścieżek per serial/film",
"rememberTrackSelectionsDescription": "Automatycznie zapisuj preferencje języka audio i napisów przy zmianie ścieżek podczas odtwarzania",
"clickVideoTogglesPlayback": "Kliknięcie wideo przełącza odtwarzanie/pauzę",
"clickVideoTogglesPlaybackDescription": "Gdy włączone, kliknięcie na odtwarzacz wideo odtwarza/wstrzymuje. W przeciwnym razie pokazuje/ukrywa kontrolki.",
"videoPlayerControls": "Kontrolki odtwarzacza wideo",
"keyboardShortcuts": "Skróty klawiszowe",
"keyboardShortcutsDescription": "Dostosuj skróty klawiszowe",
"videoPlayerNavigation": "Nawigacja odtwarzacza wideo",
"videoPlayerNavigationDescription": "Użyj klawiszy strzałek do nawigacji kontrolkami odtwarzacza",
"watchTogetherRelay": "Relay Oglądaj Razem",
"watchTogetherRelayDescription": "Ustaw niestandardowy serwer relay dla Oglądaj Razem. Wszyscy uczestnicy muszą korzystać z tego samego serwera.",
"watchTogetherRelayHint": "https://moj-relay.przyklad.pl",
"crashReporting": "Raportowanie błędów",
"crashReportingDescription": "Wysyłaj raporty o błędach, aby pomóc ulepszyć aplikację",
"debugLogging": "Logowanie debugowania",
"debugLoggingDescription": "Włącz szczegółowe logowanie do rozwiązywania problemów",
"viewLogs": "Pokaż logi",
"viewLogsDescription": "Pokaż logi aplikacji",
"clearCache": "Wyczyść pamięć podręczną",
"clearCacheDescription": "Spowoduje to wyczyszczenie wszystkich zapisanych obrazów i danych. Po wyczyszczeniu aplikacja może ładować treści wolniej.",
"clearCacheSuccess": "Pamięć podręczna wyczyszczona",
"resetSettings": "Zresetuj ustawienia",
"resetSettingsDescription": "Wszystkie ustawienia zostaną przywrócone do wartości domyślnych. Tej operacji nie można cofnąć.",
"resetSettingsSuccess": "Ustawienia zresetowane pomyślnie",
"shortcutsReset": "Skróty przywrócone do domyślnych",
"about": "O aplikacji",
"aboutDescription": "Informacje o aplikacji i licencje",
"updates": "Aktualizacje",
"updateAvailable": "Dostępna aktualizacja",
"checkForUpdates": "Sprawdź aktualizacje",
"validationErrorEnterNumber": "Wprowadź prawidłową liczbę",
"validationErrorDuration": "Czas musi być między ${min} a ${max} ${unit}",
"shortcutAlreadyAssigned": "Skrót jest już przypisany do ${action}",
"shortcutUpdated": "Skrót zaktualizowany dla ${action}",
"autoSkip": "Automatyczne pomijanie",
"autoSkipIntro": "Automatyczne pomijanie intro",
"autoSkipIntroDescription": "Automatycznie pomijaj znaczniki intro po kilku sekundach",
"autoSkipCredits": "Automatyczne pomijanie napisów końcowych",
"autoSkipCreditsDescription": "Automatycznie pomijaj napisy końcowe i odtwórz następny odcinek",
"autoSkipDelay": "Opóźnienie automatycznego pomijania",
"autoSkipDelayDescription": "Czekaj ${seconds} sekund przed automatycznym pominięciem",
"introPattern": "Wzorzec znacznika intro",
"introPatternDescription": "Wyrażenie regularne do rozpoznawania znaczników intro w tytułach rozdziałów",
"creditsPattern": "Wzorzec znacznika napisów końcowych",
"creditsPatternDescription": "Wyrażenie regularne do rozpoznawania znaczników napisów końcowych w tytułach rozdziałów",
"invalidRegex": "Nieprawidłowe wyrażenie regularne",
"downloads": "Pobrania",
"downloadLocationDescription": "Wybierz miejsce przechowywania pobranych treści",
"downloadLocationDefault": "Domyślne (Pamięć aplikacji)",
"downloadLocationCustom": "Niestandardowa lokalizacja",
"selectFolder": "Wybierz folder",
"resetToDefault": "Przywróć domyślne",
"currentPath": "Bieżąca: ${path}",
"downloadLocationChanged": "Lokalizacja pobierania zmieniona",
"downloadLocationReset": "Lokalizacja pobierania przywrócona do domyślnej",
"downloadLocationInvalid": "Wybrany folder nie jest zapisywalny",
"downloadLocationSelectError": "Nie udało się wybrać folderu",
"downloadOnWifiOnly": "Pobieraj tylko przez WiFi",
"downloadOnWifiOnlyDescription": "Blokuj pobieranie na danych komórkowych",
"autoRemoveWatchedDownloads": "Automatycznie usuwaj obejrzane pobrania",
"autoRemoveWatchedDownloadsDescription": "Automatycznie usuwaj pobrane odcinki i filmy po oznaczeniu jako obejrzane",
"cellularDownloadBlocked": "Pobieranie na danych komórkowych jest wyłączone. Połącz się z WiFi lub zmień ustawienie.",
"maxVolume": "Maksymalna głośność",
"maxVolumeDescription": "Pozwól na wzmocnienie głośności powyżej 100% dla cichych multimediów",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Pokaż, co oglądasz na Discordzie",
"trakt": "Trakt",
"traktDescription": "Synchronizuj historię oglądania z Trakt",
"companionRemoteServer": "Serwer zdalnego sterowania",
"companionRemoteServerDescription": "Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją",
"autoPip": "Automatyczny obraz w obrazie",
"autoPipDescription": "Automatycznie przejdź do trybu obraz w obrazie przy wyjściu z aplikacji podczas odtwarzania",
"matchContentFrameRate": "Dopasuj częstotliwość klatek do treści",
"matchContentFrameRateDescription": "Dostosuj częstotliwość odświeżania ekranu do treści wideo, zmniejszając drgania i oszczędzając baterię",
"matchRefreshRate": "Dopasuj częstotliwość odświeżania",
"matchRefreshRateDescription": "Przełącz częstotliwość odświeżania ekranu, aby dopasować ją do treści wideo na pełnym ekranie",
"matchDynamicRange": "Dopasuj zakres dynamiki",
"matchDynamicRangeDescription": "Automatycznie włącz HDR dla treści HDR i przywróć SDR po wyjściu z odtwarzacza",
"displaySwitchDelay": "Opóźnienie przełączania ekranu",
"tunneledPlayback": "Tunelowane odtwarzanie",
"tunneledPlaybackDescription": "Użyj sprzętowo przyspieszonego tunelowania wideo. Wyłącz, jeśli widzisz czarny ekran z dźwiękiem przy treściach HDR",
"requireProfileSelectionOnOpen": "Pytaj o profil przy otwarciu aplikacji",
"requireProfileSelectionOnOpenDescription": "Pokaż wybór profilu za każdym razem, gdy aplikacja jest otwierana",
"confirmExitOnBack": "Potwierdź przed wyjściem",
"confirmExitOnBackDescription": "Pokaż dialog potwierdzenia przy naciśnięciu wstecz, aby wyjść z aplikacji",
"forceTvMode": "Wymuś tryb TV",
"forceTvModeDescription": "Użyj układu TV niezależnie od automatycznego wykrywania. Przydatne na urządzeniach Android TV, które nie zgłaszają funkcji leanback. Po zmianie aplikacja zostanie zrestartowana.",
"autoHidePerformanceOverlay": "Automatycznie ukrywaj nakładkę wydajności",
"autoHidePerformanceOverlayDescription": "Wygaszaj nakładkę wydajności wraz z kontrolkami odtwarzania",
"showNavBarLabels": "Pokaż etykiety paska nawigacji",
"showNavBarLabelsDescription": "Wyświetl tekstowe etykiety pod ikonami paska nawigacji",
"liveTvDefaultFavorites": "Domyślnie ulubione kanały",
"liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Szukaj filmów, seriali, muzyki...",
"tryDifferentTerm": "Spróbuj innego wyszukiwania",
"searchYourMedia": "Przeszukaj swoje media",
"enterTitleActorOrKeyword": "Wprowadź tytuł, aktora lub słowo kluczowe"
},
"hotkeys": {
"setShortcutFor": "Ustaw skrót dla ${actionName}",
"clearShortcut": "Wyczyść skrót",
"actions": {
"playPause": "Odtwórz/Pauza",
"volumeUp": "Głośniej",
"volumeDown": "Ciszej",
"seekForward": "Przewiń do przodu (${seconds}s)",
"seekBackward": "Przewiń do tyłu (${seconds}s)",
"fullscreenToggle": "Pełny ekran",
"muteToggle": "Wyciszenie",
"subtitleToggle": "Napisy",
"audioTrackNext": "Następna ścieżka audio",
"subtitleTrackNext": "Następna ścieżka napisów",
"chapterNext": "Następny rozdział",
"chapterPrevious": "Poprzedni rozdział",
"speedIncrease": "Zwiększ prędkość",
"speedDecrease": "Zmniejsz prędkość",
"speedReset": "Zresetuj prędkość",
"subSeekNext": "Przewiń do następnego napisu",
"subSeekPrev": "Przewiń do poprzedniego napisu",
"shaderToggle": "Przełącz shadery",
"skipMarker": "Pomiń intro/napisy końcowe"
}
},
"fileInfo": {
"title": "Informacje o pliku",
"video": "Wideo",
"audio": "Audio",
"file": "Plik",
"advanced": "Zaawansowane",
"codec": "Kodek",
"resolution": "Rozdzielczość",
"bitrate": "Bitrate",
"frameRate": "Klatki na sekundę",
"aspectRatio": "Proporcje",
"profile": "Profil",
"bitDepth": "Głębia bitowa",
"colorSpace": "Przestrzeń kolorów",
"colorRange": "Zakres kolorów",
"colorPrimaries": "Kolory podstawowe",
"chromaSubsampling": "Subsampling chrominancji",
"channels": "Kanały",
"subtitles": "Napisy",
"overallBitrate": "Całkowity bitrate",
"path": "Ścieżka",
"size": "Rozmiar",
"container": "Kontener",
"duration": "Czas trwania",
"optimizedForStreaming": "Zoptymalizowane do strumieniowania",
"has64bitOffsets": "64-bitowe offsety"
},
"mediaMenu": {
"markAsWatched": "Oznacz jako obejrzane",
"markAsUnwatched": "Oznacz jako nieobejrzane",
"removeFromContinueWatching": "Usuń z kontynuowania oglądania",
"goToSeries": "Przejdź do serialu",
"goToSeason": "Przejdź do sezonu",
"shufflePlay": "Odtwarzanie losowe",
"fileInfo": "Informacje o pliku",
"deleteFromServer": "Usuń z serwera",
"confirmDelete": "To trwale usunie te multimedia i ich pliki z twojego serwera. Tej operacji nie można cofnąć.",
"deleteMultipleWarning": "Obejmuje to wszystkie odcinki i ich pliki.",
"mediaDeletedSuccessfully": "Element multimedialny usunięty pomyślnie",
"mediaFailedToDelete": "Nie udało się usunąć elementu multimedialnego",
"rate": "Oceń",
"playFromBeginning": "Odtwórz od początku",
"playVersion": "Odtwórz wersję..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
"mediaCardShow": "${title}, serial TV",
"mediaCardEpisode": "${title}, ${episodeInfo}",
"mediaCardSeason": "${title}, ${seasonInfo}",
"mediaCardWatched": "obejrzane",
"mediaCardPartiallyWatched": "${percent} procent obejrzane",
"mediaCardUnwatched": "nieobejrzane",
"tapToPlay": "Dotknij, aby odtworzyć"
},
"tooltips": {
"shufflePlay": "Odtwarzanie losowe",
"playTrailer": "Odtwórz zwiastun",
"markAsWatched": "Oznacz jako obejrzane",
"markAsUnwatched": "Oznacz jako nieobejrzane"
},
"videoControls": {
"audioLabel": "Audio",
"subtitlesLabel": "Napisy",
"resetToZero": "Zresetuj do 0ms",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label} odtwarza później",
"playsEarlier": "${label} odtwarza wcześniej",
"noOffset": "Bez przesunięcia",
"letterbox": "Letterbox",
"fillScreen": "Wypełnij ekran",
"stretch": "Rozciągnij",
"lockRotation": "Zablokuj obrót",
"unlockRotation": "Odblokuj obrót",
"timerActive": "Wyłącznik aktywny",
"playbackWillPauseIn": "Odtwarzanie zatrzyma się za ${duration}",
"stillWatching": "Nadal oglądasz?",
"pausingIn": "Pauza za ${seconds}s",
"continueWatching": "Kontynuuj",
"autoPlayNext": "Automatycznie odtwórz następny",
"playNext": "Odtwórz następny",
"playButton": "Odtwórz",
"pauseButton": "Pauza",
"seekBackwardButton": "Przewiń do tyłu o ${seconds} sekund",
"seekForwardButton": "Przewiń do przodu o ${seconds} sekund",
"previousButton": "Poprzedni odcinek",
"nextButton": "Następny odcinek",
"previousChapterButton": "Poprzedni rozdział",
"nextChapterButton": "Następny rozdział",
"muteButton": "Wycisz",
"unmuteButton": "Wyłącz wyciszenie",
"settingsButton": "Ustawienia wideo",
"tracksButton": "Audio i napisy",
"chaptersButton": "Rozdziały",
"versionsButton": "Wersje wideo",
"pipButton": "Tryb obraz w obrazie",
"aspectRatioButton": "Proporcje",
"ambientLighting": "Oświetlenie otoczenia",
"fullscreenButton": "Wejdź w pełny ekran",
"exitFullscreenButton": "Wyjdź z pełnego ekranu",
"alwaysOnTopButton": "Zawsze na wierzchu",
"rotationLockButton": "Blokada obrotu",
"lockScreen": "Zablokuj ekran",
"screenLockButton": "Blokada ekranu",
"longPressToUnlock": "Przytrzymaj, aby odblokować",
"timelineSlider": "Oś czasu wideo",
"volumeSlider": "Poziom głośności",
"endsAt": "Kończy się o ${time}",
"pipActive": "Odtwarzanie w trybie obraz w obrazie",
"pipFailed": "Nie udało się uruchomić trybu obraz w obrazie",
"pipErrors": {
"androidVersion": "Wymaga Androida 8.0 lub nowszego",
"iosVersion": "Wymaga iOS 15.0 lub nowszego",
"permissionDisabled": "Uprawnienie obraz w obrazie jest wyłączone. Włącz w Ustawienia > Aplikacje > Plezy > Obraz w obrazie",
"notSupported": "Urządzenie nie obsługuje trybu obraz w obrazie",
"voSwitchFailed": "Nie udało się przełączyć wyjścia wideo dla trybu obraz w obrazie",
"failed": "Nie udało się uruchomić trybu obraz w obrazie",
"unknown": "Wystąpił błąd: ${error}"
},
"chapters": "Rozdziały",
"noChaptersAvailable": "Brak dostępnych rozdziałów",
"queue": "Kolejka",
"noQueueItems": "Brak elementów w kolejce",
"searchSubtitles": "Szukaj napisów",
"language": "Język",
"noSubtitlesFound": "Nie znaleziono napisów",
"subtitleDownloaded": "Napisy pobrane",
"subtitleDownloadFailed": "Nie udało się pobrać napisów",
"searchLanguages": "Szukaj języków..."
},
"userStatus": {
"admin": "Administrator",
"restricted": "Ograniczony",
"protected": "Chroniony",
"current": "BIEŻĄCY"
},
"messages": {
"markedAsWatched": "Oznaczono jako obejrzane",
"markedAsUnwatched": "Oznaczono jako nieobejrzane",
"markedAsWatchedOffline": "Oznaczono jako obejrzane (zsynchronizuje się po połączeniu)",
"markedAsUnwatchedOffline": "Oznaczono jako nieobejrzane (zsynchronizuje się po połączeniu)",
"autoRemovedWatchedDownload": "Automatycznie usunięto: ${title}",
"removedFromContinueWatching": "Usunięto z kontynuowania oglądania",
"errorLoading": "Błąd: ${error}",
"fileInfoNotAvailable": "Informacje o pliku niedostępne",
"errorLoadingFileInfo": "Błąd ładowania informacji o pliku: ${error}",
"errorLoadingSeries": "Błąd ładowania serialu",
"errorLoadingSeason": "Błąd ładowania sezonu",
"musicNotSupported": "Odtwarzanie muzyki nie jest jeszcze obsługiwane",
"logsCleared": "Logi wyczyszczone",
"logsCopied": "Logi skopiowane do schowka",
"noLogsAvailable": "Brak dostępnych logów",
"libraryScanning": "Skanowanie \"${title}\"...",
"libraryScanStarted": "Rozpoczęto skanowanie biblioteki \"${title}\"",
"libraryScanFailed": "Nie udało się zeskanować biblioteki: ${error}",
"metadataRefreshing": "Odświeżanie metadanych \"${title}\"...",
"metadataRefreshStarted": "Rozpoczęto odświeżanie metadanych \"${title}\"",
"metadataRefreshFailed": "Nie udało się odświeżyć metadanych: ${error}",
"logoutConfirm": "Czy na pewno chcesz się wylogować?",
"noSeasonsFound": "Nie znaleziono sezonów",
"noEpisodesFound": "Nie znaleziono odcinków w pierwszym sezonie",
"noEpisodesFoundGeneral": "Nie znaleziono odcinków",
"noResultsFound": "Nie znaleziono wyników",
"sleepTimerSet": "Wyłącznik czasowy ustawiony na ${label}",
"noItemsAvailable": "Brak dostępnych elementów",
"failedToCreatePlayQueueNoItems": "Nie udało się utworzyć kolejki odtwarzania — brak elementów",
"failedPlayback": "Nie udało się ${action}: ${error}",
"switchingToCompatiblePlayer": "Przełączanie na kompatybilny odtwarzacz...",
"serverLimitTitle": "Odtwarzanie nie powiodło się",
"serverLimitBody": "Serwer zwrócił błąd (HTTP 500). Zwykle oznacza to, że właściciel serwera ustawił limit przepustowości lub transkodowania, który odrzuca Twoją sesję. Z poziomu klienta nie da się nic zrobić — właściciel serwera musi dostosować swoje ustawienia.",
"logsUploaded": "Logi przesłane",
"logsUploadFailed": "Nie udało się przesłać logów",
"logId": "ID logu"
},
"subtitlingStyling": {
"text": "Tekst",
"border": "Obramowanie",
"background": "Tło",
"fontSize": "Rozmiar czcionki",
"textColor": "Kolor tekstu",
"borderSize": "Rozmiar obramowania",
"borderColor": "Kolor obramowania",
"backgroundOpacity": "Przezroczystość tła",
"backgroundColor": "Kolor tła",
"position": "Pozycja",
"assOverride": "Nadpisywanie ASS",
"bold": "Pogrubienie",
"italic": "Kursywa"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "Zaawansowane ustawienia odtwarzacza wideo",
"presets": "Presety",
"noPresets": "Brak zapisanych presetów",
"saveAsPreset": "Zapisz jako preset...",
"presetName": "Nazwa presetu",
"presetNameHint": "Wprowadź nazwę dla tego presetu",
"loadPreset": "Załaduj",
"deletePreset": "Usuń",
"presetSaved": "Preset zapisany",
"presetLoaded": "Preset załadowany",
"presetDeleted": "Preset usunięty",
"confirmDeletePreset": "Czy na pewno chcesz usunąć ten preset?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Potwierdź działanie"
},
"discover": {
"title": "Odkryj",
"switchProfile": "Zmień profil",
"noContentAvailable": "Brak dostępnych treści",
"addMediaToLibraries": "Dodaj multimedia do swoich bibliotek",
"continueWatching": "Kontynuuj oglądanie",
"playEpisode": "S${season}E${episode}",
"overview": "Opis",
"cast": "Obsada",
"extras": "Zwiastuny i dodatki",
"studio": "Studio",
"rating": "Ocena",
"movie": "Film",
"tvShow": "Serial TV",
"minutesLeft": "${minutes} min pozostało"
},
"errors": {
"searchFailed": "Wyszukiwanie nie powiodło się: ${error}",
"connectionTimeout": "Limit czasu połączenia przy ładowaniu ${context}",
"connectionFailed": "Nie można połączyć z serwerem Plex",
"failedToLoad": "Nie udało się załadować ${context}: ${error}",
"noClientAvailable": "Brak dostępnego klienta",
"authenticationFailed": "Uwierzytelnienie nie powiodło się: ${error}",
"couldNotLaunchUrl": "Nie można otworzyć URL uwierzytelniania",
"pleaseEnterToken": "Wprowadź token",
"invalidToken": "Nieprawidłowy token",
"failedToVerifyToken": "Nie udało się zweryfikować tokena: ${error}",
"failedToSwitchProfile": "Nie udało się przełączyć na ${displayName}"
},
"libraries": {
"title": "Biblioteki",
"scanLibraryFiles": "Skanuj pliki biblioteki",
"scanLibrary": "Skanuj bibliotekę",
"analyze": "Analizuj",
"analyzeLibrary": "Analizuj bibliotekę",
"refreshMetadata": "Odśwież metadane",
"emptyTrash": "Opróżnij kosz",
"emptyingTrash": "Opróżnianie kosza dla \"${title}\"...",
"trashEmptied": "Kosz opróżniony dla \"${title}\"",
"failedToEmptyTrash": "Nie udało się opróżnić kosza: ${error}",
"analyzing": "Analizowanie \"${title}\"...",
"analysisStarted": "Analiza rozpoczęta dla \"${title}\"",
"failedToAnalyze": "Nie udało się przeanalizować biblioteki: ${error}",
"noLibrariesFound": "Nie znaleziono bibliotek",
"thisLibraryIsEmpty": "Ta biblioteka jest pusta",
"all": "Wszystkie",
"clearAll": "Wyczyść wszystko",
"scanLibraryConfirm": "Czy na pewno chcesz zeskanować \"${title}\"?",
"analyzeLibraryConfirm": "Czy na pewno chcesz przeanalizować \"${title}\"?",
"refreshMetadataConfirm": "Czy na pewno chcesz odświeżyć metadane dla \"${title}\"?",
"emptyTrashConfirm": "Czy na pewno chcesz opróżnić kosz dla \"${title}\"?",
"manageLibraries": "Zarządzaj bibliotekami",
"sort": "Sortuj",
"sortBy": "Sortuj wg",
"filters": "Filtry",
"confirmActionMessage": "Czy na pewno chcesz wykonać tę operację?",
"showLibrary": "Pokaż bibliotekę",
"hideLibrary": "Ukryj bibliotekę",
"libraryOptions": "Opcje biblioteki",
"content": "zawartość biblioteki",
"selectLibrary": "Wybierz bibliotekę",
"filtersWithCount": "Filtry (${count})",
"noRecommendations": "Brak dostępnych rekomendacji",
"noCollections": "Brak kolekcji w tej bibliotece",
"noFoldersFound": "Nie znaleziono folderów",
"folders": "foldery",
"tabs": {
"recommended": "Rekomendowane",
"browse": "Przeglądaj",
"collections": "Kolekcje",
"playlists": "Playlisty"
},
"groupings": {
"title": "Grupowanie",
"all": "Wszystkie",
"movies": "Filmy",
"shows": "Seriale TV",
"seasons": "Sezony",
"episodes": "Odcinki",
"folders": "Foldery"
}
},
"about": {
"title": "O aplikacji",
"openSourceLicenses": "Licencje open source",
"versionLabel": "Wersja ${version}",
"appDescription": "Piękny klient Plex na Flutter",
"viewLicensesDescription": "Zobacz licencje bibliotek zewnętrznych"
},
"serverSelection": {
"allServerConnectionsFailed": "Nie udało się połączyć z żadnym serwerem. Sprawdź sieć i spróbuj ponownie.",
"noServersFoundForAccount": "Nie znaleziono serwerów dla ${username} (${email})",
"failedToLoadServers": "Nie udało się załadować serwerów: ${error}"
},
"hubDetail": {
"title": "Tytuł",
"releaseYear": "Rok premiery",
"dateAdded": "Data dodania",
"rating": "Ocena",
"noItemsFound": "Nie znaleziono elementów"
},
"logs": {
"clearLogs": "Wyczyść logi",
"copyLogs": "Kopiuj logi",
"uploadLogs": "Prześlij logi"
},
"licenses": {
"relatedPackages": "Powiązane pakiety",
"license": "Licencja",
"licenseNumber": "Licencja ${number}",
"licensesCount": "${count} licencji"
},
"navigation": {
"libraries": "Biblioteki",
"downloads": "Pobrania",
"liveTv": "TV na żywo"
},
"liveTv": {
"title": "TV na żywo",
"guide": "Przewodnik",
"noChannels": "Brak dostępnych kanałów",
"noDvr": "Brak skonfigurowanego DVR na żadnym serwerze",
"noPrograms": "Brak danych o programach",
"live": "NA ŻYWO",
"reloadGuide": "Odśwież przewodnik",
"now": "Teraz",
"today": "Dzisiaj",
"midnight": "Północ",
"overnight": "Nocą",
"morning": "Rano",
"daytime": "W ciągu dnia",
"evening": "Wieczorem",
"lateNight": "Późna noc",
"whatsOn": "Co leci",
"watchChannel": "Oglądaj kanał",
"favorites": "Ulubione",
"reorderFavorites": "Zmień kolejność ulubionych",
"joinSession": "Dołącz do trwającej sesji",
"watchFromStart": "Oglądaj od początku (${minutes} min temu)",
"watchLive": "Oglądaj na żywo",
"goToLive": "Przejdź do na żywo"
},
"collections": {
"title": "Kolekcje",
"collection": "Kolekcja",
"empty": "Kolekcja jest pusta",
"unknownLibrarySection": "Nie można usunąć: Nieznana sekcja biblioteki",
"deleteCollection": "Usuń kolekcję",
"deleteConfirm": "Czy na pewno chcesz usunąć \"${title}\"? Tej operacji nie można cofnąć.",
"deleted": "Kolekcja usunięta",
"deleteFailed": "Nie udało się usunąć kolekcji",
"deleteFailedWithError": "Nie udało się usunąć kolekcji: ${error}",
"failedToLoadItems": "Nie udało się załadować elementów kolekcji: ${error}",
"selectCollection": "Wybierz kolekcję",
"collectionName": "Nazwa kolekcji",
"enterCollectionName": "Wprowadź nazwę kolekcji",
"addedToCollection": "Dodano do kolekcji",
"errorAddingToCollection": "Nie udało się dodać do kolekcji",
"created": "Kolekcja utworzona",
"removeFromCollection": "Usuń z kolekcji",
"removeFromCollectionConfirm": "Usunąć \"${title}\" z tej kolekcji?",
"removedFromCollection": "Usunięto z kolekcji",
"removeFromCollectionFailed": "Nie udało się usunąć z kolekcji",
"removeFromCollectionError": "Błąd usuwania z kolekcji: ${error}",
"searchCollections": "Szukaj kolekcji..."
},
"playlists": {
"title": "Playlisty",
"playlist": "Playlista",
"noPlaylists": "Nie znaleziono playlist",
"create": "Utwórz playlistę",
"playlistName": "Nazwa playlisty",
"enterPlaylistName": "Wprowadź nazwę playlisty",
"delete": "Usuń playlistę",
"removeItem": "Usuń z playlisty",
"smartPlaylist": "Inteligentna playlista",
"itemCount": "${count} elementów",
"oneItem": "1 element",
"emptyPlaylist": "Ta playlista jest pusta",
"deleteConfirm": "Usunąć playlistę?",
"deleteMessage": "Czy na pewno chcesz usunąć \"${name}\"?",
"created": "Playlista utworzona",
"deleted": "Playlista usunięta",
"itemAdded": "Dodano do playlisty",
"itemRemoved": "Usunięto z playlisty",
"selectPlaylist": "Wybierz playlistę",
"errorCreating": "Nie udało się utworzyć playlisty",
"errorDeleting": "Nie udało się usunąć playlisty",
"errorLoading": "Nie udało się załadować playlist",
"errorAdding": "Nie udało się dodać do playlisty",
"errorReordering": "Nie udało się zmienić kolejności elementu playlisty",
"errorRemoving": "Nie udało się usunąć z playlisty"
},
"watchTogether": {
"title": "Oglądaj razem",
"description": "Oglądaj treści zsynchronizowane z przyjaciółmi i rodziną",
"createSession": "Utwórz sesję",
"creating": "Tworzenie...",
"joinSession": "Dołącz do sesji",
"joining": "Dołączanie...",
"controlMode": "Tryb kontroli",
"controlModeQuestion": "Kto może kontrolować odtwarzanie?",
"hostOnly": "Tylko host",
"anyone": "Każdy",
"hostingSession": "Hostowanie sesji",
"inSession": "W sesji",
"sessionCode": "Kod sesji",
"hostControlsPlayback": "Host kontroluje odtwarzanie",
"anyoneCanControl": "Każdy może kontrolować odtwarzanie",
"hostControls": "Kontrola hosta",
"anyoneControls": "Kontrola każdego",
"participants": "Uczestnicy",
"host": "Host",
"hostBadge": "HOST",
"youAreHost": "Jesteś hostem",
"watchingWithOthers": "Oglądasz z innymi",
"endSession": "Zakończ sesję",
"leaveSession": "Opuść sesję",
"endSessionQuestion": "Zakończyć sesję?",
"leaveSessionQuestion": "Opuścić sesję?",
"endSessionConfirm": "To zakończy sesję dla wszystkich uczestników.",
"leaveSessionConfirm": "Zostaniesz usunięty z sesji.",
"endSessionConfirmOverlay": "To zakończy sesję oglądania dla wszystkich uczestników.",
"leaveSessionConfirmOverlay": "Zostaniesz odłączony od sesji oglądania.",
"end": "Zakończ",
"leave": "Opuść",
"syncing": "Synchronizacja...",
"joinWatchSession": "Dołącz do sesji oglądania",
"enterCodeHint": "Wprowadź 5-znakowy kod",
"pasteFromClipboard": "Wklej ze schowka",
"pleaseEnterCode": "Wprowadź kod sesji",
"codeMustBe5Chars": "Kod sesji musi mieć 5 znaków",
"joinInstructions": "Wprowadź kod sesji udostępniony przez hosta, aby dołączyć do sesji oglądania.",
"failedToCreate": "Nie udało się utworzyć sesji",
"failedToJoin": "Nie udało się dołączyć do sesji",
"sessionCodeCopied": "Kod sesji skopiowany do schowka",
"relayUnreachable": "Serwer przekaźnika jest nieosiągalny. Może to być spowodowane blokadą przez twojego dostawcę internetu. Możesz spróbować, ale Oglądaj razem może nie działać.",
"reconnectingToHost": "Ponowne łączenie z hostem...",
"currentPlayback": "Bieżące odtwarzanie",
"joinCurrentPlayback": "Dołącz do bieżącego odtwarzania",
"joinCurrentPlaybackDescription": "Wróć do tego, co host aktualnie ogląda",
"failedToOpenCurrentPlayback": "Nie udało się otworzyć bieżącego odtwarzania",
"participantJoined": "${name} dołączył",
"participantLeft": "${name} opuścił",
"participantPaused": "${name} wstrzymał",
"participantResumed": "${name} wznowił",
"participantSeeked": "${name} przewinął",
"participantBuffering": "${name} buforuje",
"waitingForParticipants": "Oczekiwanie na załadowanie u innych...",
"recentRooms": "Ostatnie pokoje",
"renameRoom": "Zmień nazwę pokoju",
"removeRoom": "Usuń"
},
"downloads": {
"title": "Pobrania",
"manage": "Zarządzaj",
"tvShows": "Seriale TV",
"movies": "Filmy",
"noDownloads": "Brak pobrań",
"noDownloadsDescription": "Pobrane treści pojawią się tutaj do oglądania offline",
"downloadNow": "Pobierz",
"deleteDownload": "Usuń pobranie",
"retryDownload": "Ponów pobieranie",
"downloadQueued": "Pobranie w kolejce",
"serverErrorBitrate": "Błąd serwera — plik może przekraczać limit bitrate zdalnego streamingu",
"episodesQueued": "${count} odcinków w kolejce pobierania",
"downloadDeleted": "Pobranie usunięte",
"deleteConfirm": "Czy na pewno chcesz usunąć \"${title}\"? Spowoduje to usunięcie pobranego pliku z urządzenia.",
"deletingWithProgress": "Usuwanie ${title}... (${current} z ${total})",
"noDownloadsTree": "Brak pobrań",
"pauseAll": "Wstrzymaj wszystko",
"resumeAll": "Wznów wszystko",
"deleteAll": "Usuń wszystko",
"selectVersion": "Wybierz wersję",
"allEpisodes": "Wszystkie odcinki",
"unwatchedOnly": "Tylko nieobejrzane",
"nextNUnwatched": "Następne ${count} nieobejrzanych",
"customAmount": "Własna ilość...",
"howManyEpisodes": "Ile odcinków?",
"itemsQueued": "${count} elementów dodanych do kolejki pobierania",
"keepSynced": "Synchronizuj na bieżąco",
"downloadOnce": "Pobierz raz",
"keepNUnwatched": "Zachowaj ${count} nieobejrzanych",
"editSyncRule": "Edytuj regułę synchronizacji",
"removeSyncRule": "Usuń regułę synchronizacji",
"removeSyncRuleConfirm": "Zatrzymać synchronizację \"${title}\"? Pobrane odcinki zostaną zachowane.",
"syncRuleCreated": "Reguła synchronizacji utworzona — zachowywanie ${count} nieobejrzanych odcinków",
"syncRuleUpdated": "Reguła synchronizacji zaktualizowana",
"syncRuleRemoved": "Reguła synchronizacji usunięta",
"syncedNewEpisodes": "Zsynchronizowano ${count} nowych odcinków dla ${title}",
"activeSyncRules": "Reguły synchronizacji",
"noSyncRules": "Brak reguł synchronizacji",
"manageSyncRule": "Zarządzaj synchronizacją",
"editEpisodeCount": "Liczba odcinków",
"editSyncFilter": "Filtr synchronizacji",
"syncAllItems": "Synchronizuję wszystkie elementy",
"syncUnwatchedItems": "Synchronizuję nieobejrzane elementy",
"syncRuleListCreated": "Utworzono regułę synchronizacji"
},
"shaders": {
"title": "Shadery",
"noShaderDescription": "Bez ulepszenia wideo",
"nvscalerDescription": "Skalowanie obrazu NVIDIA dla ostrzejszego wideo",
"qualityFast": "Szybki",
"qualityHQ": "Wysoka jakość",
"mode": "Tryb",
"importShader": "Importuj shader",
"customShaderDescription": "Niestandardowy shader GLSL",
"shaderImported": "Shader zaimportowany",
"shaderImportFailed": "Nie udało się zaimportować shadera",
"deleteShader": "Usuń shader",
"deleteShaderConfirm": "Usunąć \"${name}\"?"
},
"companionRemote": {
"title": "Pilot zdalny",
"connectedTo": "Połączono z ${name}",
"session": {
"startingServer": "Uruchamianie serwera zdalnego...",
"failedToCreate": "Nie udało się uruchomić serwera zdalnego:",
"hostAddress": "Adres hosta",
"connected": "Połączono",
"serverRunning": "Serwer zdalny aktywny",
"serverStopped": "Serwer zdalny zatrzymany",
"serverRunningDescription": "Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć",
"serverStoppedDescription": "Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych",
"usePhoneToControl": "Użyj urządzenia mobilnego, aby sterować tą aplikacją",
"startServer": "Uruchom serwer",
"stopServer": "Zatrzymaj serwer",
"minimize": "Minimalizuj"
},
"pairing": {
"discoveryDescription": "Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "Łączenie...",
"searchingForDevices": "Szukanie urządzeń...",
"noDevicesFound": "Nie znaleziono urządzeń w sieci",
"noDevicesHint": "Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi",
"availableDevices": "Dostępne urządzenia",
"manualConnection": "Połączenie ręczne",
"cryptoInitFailed": "Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.",
"validationHostRequired": "Wprowadź adres hosta",
"validationHostFormat": "Format musi być IP:port (np. 192.168.1.100:48632)",
"connectionTimedOut": "Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.",
"sessionNotFound": "Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.",
"authFailed": "Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.",
"failedToConnect": "Nie udało się połączyć: ${error}"
},
"remote": {
"disconnectConfirm": "Czy chcesz się rozłączyć od sesji zdalnej?",
"reconnecting": "Ponowne łączenie...",
"attemptOf": "Próba ${current} z 5",
"retryNow": "Ponów teraz",
"tabRemote": "Pilot",
"tabPlay": "Odtwórz",
"tabMore": "Więcej",
"menu": "Menu",
"tabNavigation": "Nawigacja",
"tabDiscover": "Odkryj",
"tabLibraries": "Biblioteki",
"tabSearch": "Szukaj",
"tabDownloads": "Pobrania",
"tabSettings": "Ustawienia",
"previous": "Poprzedni",
"playPause": "Odtwórz/Pauza",
"next": "Następny",
"seekBack": "Przewiń wstecz",
"stop": "Stop",
"seekForward": "Przewiń w przód",
"volume": "Głośność",
"volumeDown": "Ciszej",
"volumeUp": "Głośniej",
"fullscreen": "Pełny ekran",
"subtitles": "Napisy",
"audio": "Audio",
"searchHint": "Szukaj na komputerze..."
}
},
"videoSettings": {
"playbackSettings": "Ustawienia odtwarzania",
"playbackSpeed": "Prędkość odtwarzania",
"sleepTimer": "Wyłącznik czasowy",
"audioSync": "Synchronizacja audio",
"subtitleSync": "Synchronizacja napisów",
"hdr": "HDR",
"audioOutput": "Wyjście audio",
"performanceOverlay": "Nakładka wydajności",
"audioPassthrough": "Bezpośrednie audio",
"audioNormalization": "Normalizacja audio"
},
"externalPlayer": {
"title": "Zewnętrzny odtwarzacz",
"useExternalPlayer": "Użyj zewnętrznego odtwarzacza",
"useExternalPlayerDescription": "Otwieraj wideo w zewnętrznej aplikacji zamiast wbudowanego odtwarzacza",
"selectPlayer": "Wybierz odtwarzacz",
"customPlayers": "Niestandardowe odtwarzacze",
"systemDefault": "Domyślny systemowy",
"addCustomPlayer": "Dodaj niestandardowy odtwarzacz",
"playerName": "Nazwa odtwarzacza",
"playerCommand": "Polecenie",
"playerPackage": "Nazwa pakietu",
"playerUrlScheme": "Schemat URL",
"off": "Wyłączony",
"launchFailed": "Nie udało się otworzyć zewnętrznego odtwarzacza",
"appNotInstalled": "${name} nie jest zainstalowany",
"playInExternalPlayer": "Odtwórz w zewnętrznym odtwarzaczu"
},
"metadataEdit": {
"editMetadata": "Edytuj...",
"screenTitle": "Edytuj metadane",
"basicInfo": "Podstawowe informacje",
"artwork": "Grafika",
"advancedSettings": "Ustawienia zaawansowane",
"title": "Tytuł",
"sortTitle": "Tytuł do sortowania",
"originalTitle": "Tytuł oryginalny",
"releaseDate": "Data premiery",
"contentRating": "Klasyfikacja wiekowa",
"studio": "Studio",
"tagline": "Tagline",
"summary": "Opis",
"poster": "Plakat",
"background": "Tło",
"logo": "Logo",
"squareArt": "Kwadratowy obraz",
"selectPoster": "Wybierz plakat",
"selectBackground": "Wybierz tło",
"selectLogo": "Wybierz logo",
"selectSquareArt": "Wybierz kwadratowy obraz",
"fromUrl": "Z URL",
"uploadFile": "Prześlij plik",
"enterImageUrl": "Wprowadź URL obrazu",
"imageUrl": "URL obrazu",
"metadataUpdated": "Metadane zaktualizowane",
"metadataUpdateFailed": "Nie udało się zaktualizować metadanych",
"artworkUpdated": "Grafika zaktualizowana",
"artworkUpdateFailed": "Nie udało się zaktualizować grafiki",
"noArtworkAvailable": "Brak dostępnej grafiki",
"notSet": "Nie ustawiono",
"libraryDefault": "Domyślne biblioteki",
"accountDefault": "Domyślne konta",
"seriesDefault": "Domyślne serialu",
"episodeSorting": "Sortowanie odcinków",
"oldestFirst": "Najstarsze najpierw",
"newestFirst": "Najnowsze najpierw",
"keep": "Zachowaj",
"allEpisodes": "Wszystkie odcinki",
"latestEpisodes": "${count} najnowszych odcinków",
"latestEpisode": "Najnowszy odcinek",
"episodesAddedPastDays": "Odcinki dodane w ciągu ostatnich ${count} dni",
"deleteAfterPlaying": "Usuń odcinki po odtworzeniu",
"never": "Nigdy",
"afterADay": "Po jednym dniu",
"afterAWeek": "Po tygodniu",
"afterAMonth": "Po miesiącu",
"onNextRefresh": "Przy następnym odświeżeniu",
"seasons": "Sezony",
"show": "Pokaż",
"hide": "Ukryj",
"episodeOrdering": "Kolejność odcinków",
"tmdbAiring": "The Movie Database (Emisja)",
"tvdbAiring": "TheTVDB (Emisja)",
"tvdbAbsolute": "TheTVDB (Absolutna)",
"metadataLanguage": "Język metadanych",
"useOriginalTitle": "Użyj oryginalnego tytułu",
"preferredAudioLanguage": "Preferowany język audio",
"preferredSubtitleLanguage": "Preferowany język napisów",
"subtitleMode": "Tryb automatycznego wyboru napisów",
"manuallySelected": "Wybrany ręcznie",
"shownWithForeignAudio": "Wyświetlane przy obcojęzycznym audio",
"alwaysEnabled": "Zawsze włączone",
"tags": "Tagi",
"addTag": "Dodaj tag",
"genre": "Gatunek",
"director": "Reżyser",
"writer": "Scenarzysta",
"producer": "Producent",
"country": "Kraj",
"collection": "Kolekcja",
"label": "Etykieta",
"style": "Styl",
"mood": "Nastrój"
},
"serverTasks": {
"title": "Zadania serwera",
"failedToLoad": "Nie udało się załadować zadań",
"noTasks": "Brak uruchomionych zadań"
},
"trakt": {
"title": "Trakt",
"connected": "Połączono",
"connectedAs": "Połączono jako @${username}",
"disconnectConfirm": "Rozłączyć konto Trakt?",
"disconnectConfirmBody": "Plezy przestanie wysyłać zdarzenia odtwarzania do Trakt. Możesz połączyć się ponownie w dowolnej chwili.",
"scrobble": "Scrobbling w czasie rzeczywistym",
"scrobbleDescription": "Wysyłaj zdarzenia odtwarzania, pauzy i zatrzymania do Trakt podczas odtwarzania.",
"watchedSync": "Synchronizuj status obejrzane",
"watchedSyncDescription": "Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt.",
"deviceCodeTitle": "Aktywuj Plezy w Trakt",
"deviceCodeBody": "Otwórz ${url} i wpisz ten kod:",
"openTraktActivate": "Otwórz Trakt, aby aktywować",
"waitingForAuthorization": "Czekanie na autoryzację…",
"codeCopied": "Kod skopiowany",
"connectFailed": "Nie udało się połączyć z Trakt. Spróbuj ponownie."
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Entrar com Plex",
"showQRCode": "Mostrar QR Code",
"authenticate": "Autenticar",
"authenticationTimeout": "A autenticação expirou. Tente novamente.",
"scanQRToSignIn": "Escaneie este QR code para entrar",
"waitingForAuth": "Aguardando autenticação...\nConclua o login no seu navegador.",
"useBrowser": "Usar navegador"
},
"common": {
"cancel": "Cancelar",
"save": "Salvar",
"close": "Fechar",
"clear": "Limpar",
"reset": "Redefinir",
"later": "Depois",
"submit": "Enviar",
"confirm": "Confirmar",
"retry": "Tentar novamente",
"logout": "Sair",
"unknown": "Desconhecido",
"refresh": "Atualizar",
"yes": "Sim",
"no": "Não",
"delete": "Excluir",
"shuffle": "Aleatório",
"addTo": "Adicionar a...",
"createNew": "Criar novo",
"connect": "Conectar",
"disconnect": "Desconectar",
"play": "Reproduzir",
"pause": "Pausar",
"resume": "Retomar",
"error": "Erro",
"search": "Buscar",
"home": "Início",
"back": "Voltar",
"settings": "Configurações",
"mute": "Silenciar",
"ok": "OK",
"reconnect": "Reconectar",
"exitConfirmTitle": "Sair do app?",
"exitConfirmMessage": "Tem certeza que deseja sair?",
"dontAskAgain": "Não perguntar novamente",
"exit": "Sair",
"viewAll": "Ver Tudo",
"checkingNetwork": "Verificando rede...",
"refreshingServers": "Atualizando servidores...",
"loadingServers": "Carregando servidores...",
"connectingToServers": "Conectando aos servidores...",
"startingOfflineMode": "Iniciando modo offline...",
"loading": "Carregando..."
},
"screens": {
"licenses": "Licenças",
"switchProfile": "Trocar Perfil",
"subtitleStyling": "Estilo de Legendas",
"mpvConfig": "mpv.conf",
"logs": "Logs"
},
"update": {
"available": "Atualização Disponível",
"versionAvailable": "Versão ${version} está disponível",
"currentVersion": "Atual: ${version}",
"skipVersion": "Pular Esta Versão",
"viewRelease": "Ver Lançamento",
"latestVersion": "Você está na versão mais recente",
"checkFailed": "Falha ao verificar atualizações"
},
"settings": {
"title": "Configurações",
"supportDeveloper": "Apoie o Plezy",
"supportDeveloperDescription": "Doe via Liberapay para financiar o desenvolvimento",
"language": "Idioma",
"theme": "Tema",
"appearance": "Aparência",
"videoPlayback": "Reprodução de Vídeo",
"videoPlaybackDescription": "Configurar comportamento de reprodução",
"advanced": "Avançado",
"episodePosterMode": "Estilo do Poster de Episódio",
"seriesPoster": "Poster da Série",
"seasonPoster": "Poster da Temporada",
"episodeThumbnail": "Miniatura",
"showHeroSectionDescription": "Exibir carrossel de conteúdo em destaque na tela inicial",
"secondsLabel": "Segundos",
"minutesLabel": "Minutos",
"secondsShort": "s",
"minutesShort": "m",
"durationHint": "Insira a duração (${min}-${max})",
"systemTheme": "Sistema",
"lightTheme": "Claro",
"darkTheme": "Escuro",
"oledTheme": "OLED",
"libraryDensity": "Densidade da Biblioteca",
"compact": "Compacto",
"comfortable": "Confortável",
"viewMode": "Modo de Visualização",
"gridView": "Grade",
"listView": "Lista",
"showHeroSection": "Mostrar Seção de Destaque",
"useGlobalHubs": "Usar Layout Plex Home",
"useGlobalHubsDescription": "Mostrar hubs da página inicial como o cliente oficial Plex. Quando desativado, mostra recomendações por biblioteca.",
"showServerNameOnHubs": "Mostrar Nome do Servidor nos Hubs",
"showServerNameOnHubsDescription": "Sempre exibir o nome do servidor nos títulos dos hubs. Quando desativado, mostra apenas para nomes duplicados.",
"alwaysKeepSidebarOpen": "Manter Barra Lateral Sempre Aberta",
"alwaysKeepSidebarOpenDescription": "A barra lateral fica expandida e a área de conteúdo se ajusta",
"showUnwatchedCount": "Mostrar Contagem de Não Assistidos",
"showUnwatchedCountDescription": "Exibir contagem de episódios não assistidos em séries e temporadas",
"hideSpoilers": "Ocultar Spoilers de Episódios Não Assistidos",
"hideSpoilersDescription": "Desfocar miniaturas e ocultar descrições de episódios que você ainda não assistiu",
"playerBackend": "Backend do Player",
"exoPlayer": "ExoPlayer (Recomendado)",
"mpv": "mpv",
"hardwareDecoding": "Decodificação por Hardware",
"hardwareDecodingDescription": "Usar aceleração por hardware quando disponível",
"bufferSize": "Tamanho do Buffer",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automático (Recomendado)",
"bufferSizeWarning": "Seu dispositivo tem ${heap}MB de memória. Um buffer de ${size}MB pode causar problemas de reprodução.",
"subtitleStyling": "Estilo de Legendas",
"subtitleStylingDescription": "Personalizar aparência das legendas",
"smallSkipDuration": "Duração do Avanço Curto",
"largeSkipDuration": "Duração do Avanço Longo",
"rewindOnResume": "Rebobinar ao retomar",
"secondsUnit": "${seconds} segundos",
"defaultSleepTimer": "Timer de Sono Padrão",
"minutesUnit": "${minutes} minutos",
"rememberTrackSelections": "Lembrar seleção de faixas por série/filme",
"rememberTrackSelectionsDescription": "Salvar automaticamente preferências de idioma de áudio e legenda ao trocar faixas durante a reprodução",
"clickVideoTogglesPlayback": "Clicar no vídeo para alternar reprodução/pausa",
"clickVideoTogglesPlaybackDescription": "Se ativado, clicar no player reproduz/pausa o vídeo. Caso contrário, clicar mostra/oculta os controles.",
"videoPlayerControls": "Controles do Player de Vídeo",
"keyboardShortcuts": "Atalhos de Teclado",
"keyboardShortcutsDescription": "Personalizar atalhos de teclado",
"videoPlayerNavigation": "Navegação do Player de Vídeo",
"videoPlayerNavigationDescription": "Usar teclas de seta para navegar nos controles do player",
"watchTogetherRelay": "Relay do Assistir Juntos",
"watchTogetherRelayDescription": "Definir um servidor relay personalizado para Assistir Juntos. Todos os participantes devem usar o mesmo servidor.",
"watchTogetherRelayHint": "https://meu-relay.exemplo.com.br",
"crashReporting": "Relatório de Erros",
"crashReportingDescription": "Enviar relatórios de erros para ajudar a melhorar o app",
"debugLogging": "Log de Depuração",
"debugLoggingDescription": "Ativar log detalhado para solução de problemas",
"viewLogs": "Ver Logs",
"viewLogsDescription": "Ver logs do aplicativo",
"clearCache": "Limpar Cache",
"clearCacheDescription": "Isso limpará todas as imagens e dados em cache. O app pode demorar mais para carregar conteúdo após limpar o cache.",
"clearCacheSuccess": "Cache limpo com sucesso",
"resetSettings": "Redefinir Configurações",
"resetSettingsDescription": "Isso redefinirá todas as configurações para os valores padrão. Esta ação não pode ser desfeita.",
"resetSettingsSuccess": "Configurações redefinidas com sucesso",
"shortcutsReset": "Atalhos redefinidos para o padrão",
"about": "Sobre",
"aboutDescription": "Informações do app e licenças",
"updates": "Atualizações",
"updateAvailable": "Atualização Disponível",
"checkForUpdates": "Verificar Atualizações",
"validationErrorEnterNumber": "Insira um número válido",
"validationErrorDuration": "A duração deve ser entre ${min} e ${max} ${unit}",
"shortcutAlreadyAssigned": "Atalho já atribuído a ${action}",
"shortcutUpdated": "Atalho atualizado para ${action}",
"autoSkip": "Pular Automaticamente",
"autoSkipIntro": "Pular Intro Automaticamente",
"autoSkipIntroDescription": "Pular marcadores de intro automaticamente após alguns segundos",
"autoSkipCredits": "Pular Créditos Automaticamente",
"autoSkipCreditsDescription": "Pular créditos automaticamente e reproduzir próximo episódio",
"autoSkipDelay": "Atraso do Pulo Automático",
"autoSkipDelayDescription": "Aguardar ${seconds} segundos antes de pular automaticamente",
"introPattern": "Padrão de marcador de intro",
"introPatternDescription": "Expressão regular para corresponder marcadores de intro nos títulos dos capítulos",
"creditsPattern": "Padrão de marcador de créditos",
"creditsPatternDescription": "Expressão regular para corresponder marcadores de créditos nos títulos dos capítulos",
"invalidRegex": "Expressão regular inválida",
"downloads": "Downloads",
"downloadLocationDescription": "Escolha onde armazenar conteúdo baixado",
"downloadLocationDefault": "Padrão (Armazenamento do App)",
"downloadLocationCustom": "Local Personalizado",
"selectFolder": "Selecionar Pasta",
"resetToDefault": "Redefinir para Padrão",
"currentPath": "Atual: ${path}",
"downloadLocationChanged": "Local de download alterado",
"downloadLocationReset": "Local de download redefinido para padrão",
"downloadLocationInvalid": "A pasta selecionada não permite gravação",
"downloadLocationSelectError": "Falha ao selecionar pasta",
"downloadOnWifiOnly": "Baixar apenas no WiFi",
"downloadOnWifiOnlyDescription": "Impedir downloads quando em dados móveis",
"autoRemoveWatchedDownloads": "Remover downloads assistidos automaticamente",
"autoRemoveWatchedDownloadsDescription": "Excluir automaticamente episódios e filmes baixados quando marcados como assistidos",
"cellularDownloadBlocked": "Downloads estão desativados em dados móveis. Conecte ao WiFi ou altere a configuração.",
"maxVolume": "Volume Máximo",
"maxVolumeDescription": "Permitir aumento de volume acima de 100% para mídias silenciosas",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Mostrar o que você está assistindo no Discord",
"trakt": "Trakt",
"traktDescription": "Sincronizar histórico de exibição com Trakt",
"companionRemoteServer": "Servidor de controlo remoto",
"companionRemoteServerDescription": "Permitir que dispositivos móveis na sua rede controlem esta aplicação",
"autoPip": "Picture-in-Picture Automático",
"autoPipDescription": "Entrar automaticamente em picture-in-picture ao sair do app durante a reprodução",
"matchContentFrameRate": "Corresponder Taxa de Quadros do Conteúdo",
"matchContentFrameRateDescription": "Ajustar a taxa de atualização da tela para corresponder ao conteúdo de vídeo, reduzindo tremulação e economizando bateria",
"matchRefreshRate": "Corresponder Taxa de Atualização",
"matchRefreshRateDescription": "Alterar a taxa de atualização da tela para corresponder ao conteúdo de vídeo em tela cheia",
"matchDynamicRange": "Corresponder Faixa Dinâmica",
"matchDynamicRangeDescription": "Ativar automaticamente o HDR para conteúdo HDR e voltar ao SDR ao sair do reprodutor",
"displaySwitchDelay": "Atraso na Troca de Tela",
"tunneledPlayback": "Reprodução Tunelizada",
"tunneledPlaybackDescription": "Usar tunelamento de vídeo acelerado por hardware. Desative se você vir uma tela preta com áudio em conteúdo HDR",
"requireProfileSelectionOnOpen": "Pedir perfil ao abrir o app",
"requireProfileSelectionOnOpenDescription": "Mostrar seleção de perfil toda vez que o app for aberto",
"confirmExitOnBack": "Confirmar antes de sair",
"confirmExitOnBackDescription": "Mostrar diálogo de confirmação ao pressionar voltar para sair do app",
"forceTvMode": "Forçar modo TV",
"forceTvModeDescription": "Usar o layout de TV independentemente da detecção automática. Útil em dispositivos Android TV que não reportam o recurso leanback. Reinicia o app ao alterar.",
"autoHidePerformanceOverlay": "Ocultar overlay de desempenho automaticamente",
"autoHidePerformanceOverlayDescription": "Desvanecer o overlay de desempenho com os controles de reprodução",
"showNavBarLabels": "Mostrar Rótulos da Barra de Navegação",
"showNavBarLabelsDescription": "Exibir rótulos de texto sob os ícones da barra de navegação",
"liveTvDefaultFavorites": "Canais favoritos por padrão",
"liveTvDefaultFavoritesDescription": "Mostrar apenas canais favoritos ao abrir TV ao vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Buscar filmes, séries, músicas...",
"tryDifferentTerm": "Tente um termo de busca diferente",
"searchYourMedia": "Buscar suas mídias",
"enterTitleActorOrKeyword": "Insira um título, ator ou palavra-chave"
},
"hotkeys": {
"setShortcutFor": "Definir Atalho para ${actionName}",
"clearShortcut": "Limpar atalho",
"actions": {
"playPause": "Reproduzir/Pausar",
"volumeUp": "Aumentar Volume",
"volumeDown": "Diminuir Volume",
"seekForward": "Avançar (${seconds}s)",
"seekBackward": "Retroceder (${seconds}s)",
"fullscreenToggle": "Alternar Tela Cheia",
"muteToggle": "Alternar Silêncio",
"subtitleToggle": "Alternar Legendas",
"audioTrackNext": "Próxima Faixa de Áudio",
"subtitleTrackNext": "Próxima Faixa de Legenda",
"chapterNext": "Próximo Capítulo",
"chapterPrevious": "Capítulo Anterior",
"speedIncrease": "Aumentar Velocidade",
"speedDecrease": "Diminuir Velocidade",
"speedReset": "Redefinir Velocidade",
"subSeekNext": "Ir para Próxima Legenda",
"subSeekPrev": "Ir para Legenda Anterior",
"shaderToggle": "Alternar Shaders",
"skipMarker": "Pular Intro/Créditos"
}
},
"fileInfo": {
"title": "Info do Arquivo",
"video": "Vídeo",
"audio": "Áudio",
"file": "Arquivo",
"advanced": "Avançado",
"codec": "Codec",
"resolution": "Resolução",
"bitrate": "Taxa de Bits",
"frameRate": "Taxa de Quadros",
"aspectRatio": "Proporção",
"profile": "Perfil",
"bitDepth": "Profundidade de Bits",
"colorSpace": "Espaço de Cor",
"colorRange": "Faixa de Cor",
"colorPrimaries": "Primárias de Cor",
"chromaSubsampling": "Subamostragem de Croma",
"channels": "Canais",
"subtitles": "Legendas",
"overallBitrate": "Taxa de bits total",
"path": "Caminho",
"size": "Tamanho",
"container": "Container",
"duration": "Duração",
"optimizedForStreaming": "Otimizado para Streaming",
"has64bitOffsets": "Offsets de 64 bits"
},
"mediaMenu": {
"markAsWatched": "Marcar como Assistido",
"markAsUnwatched": "Marcar como Não Assistido",
"removeFromContinueWatching": "Remover de Continuar Assistindo",
"goToSeries": "Ir para a série",
"goToSeason": "Ir para a temporada",
"shufflePlay": "Reprodução Aleatória",
"fileInfo": "Info do Arquivo",
"deleteFromServer": "Excluir do servidor",
"confirmDelete": "Isso excluirá permanentemente esta mídia e seus arquivos do seu servidor. Esta ação não pode ser desfeita.",
"deleteMultipleWarning": "Isso inclui todos os episódios e seus arquivos.",
"mediaDeletedSuccessfully": "Item de mídia excluído com sucesso",
"mediaFailedToDelete": "Falha ao excluir item de mídia",
"rate": "Avaliar",
"playFromBeginning": "Reproduzir do início",
"playVersion": "Reproduzir versão..."
},
"accessibility": {
"mediaCardMovie": "${title}, filme",
"mediaCardShow": "${title}, série de TV",
"mediaCardEpisode": "${title}, ${episodeInfo}",
"mediaCardSeason": "${title}, ${seasonInfo}",
"mediaCardWatched": "assistido",
"mediaCardPartiallyWatched": "${percent} por cento assistido",
"mediaCardUnwatched": "não assistido",
"tapToPlay": "Toque para reproduzir"
},
"tooltips": {
"shufflePlay": "Reprodução aleatória",
"playTrailer": "Reproduzir trailer",
"markAsWatched": "Marcar como assistido",
"markAsUnwatched": "Marcar como não assistido"
},
"videoControls": {
"audioLabel": "Áudio",
"subtitlesLabel": "Legendas",
"resetToZero": "Redefinir para 0ms",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label} reproduz depois",
"playsEarlier": "${label} reproduz antes",
"noOffset": "Sem deslocamento",
"letterbox": "Letterbox",
"fillScreen": "Preencher tela",
"stretch": "Esticar",
"lockRotation": "Travar rotação",
"unlockRotation": "Destravar rotação",
"timerActive": "Timer Ativo",
"playbackWillPauseIn": "A reprodução pausará em ${duration}",
"stillWatching": "Ainda assistindo?",
"pausingIn": "Pausando em ${seconds}s",
"continueWatching": "Continuar",
"autoPlayNext": "Reproduzir Próximo Automaticamente",
"playNext": "Reproduzir Próximo",
"playButton": "Reproduzir",
"pauseButton": "Pausar",
"seekBackwardButton": "Retroceder ${seconds} segundos",
"seekForwardButton": "Avançar ${seconds} segundos",
"previousButton": "Episódio anterior",
"nextButton": "Próximo episódio",
"previousChapterButton": "Capítulo anterior",
"nextChapterButton": "Próximo capítulo",
"muteButton": "Silenciar",
"unmuteButton": "Ativar som",
"settingsButton": "Configurações de vídeo",
"tracksButton": "Áudio e Legendas",
"chaptersButton": "Capítulos",
"versionsButton": "Versões do vídeo",
"pipButton": "Modo Picture-in-Picture",
"aspectRatioButton": "Proporção",
"ambientLighting": "Iluminação ambiente",
"fullscreenButton": "Entrar em tela cheia",
"exitFullscreenButton": "Sair da tela cheia",
"alwaysOnTopButton": "Sempre no topo",
"rotationLockButton": "Travar rotação",
"lockScreen": "Travar tela",
"screenLockButton": "Travar tela",
"longPressToUnlock": "Pressione e segure para destravar",
"timelineSlider": "Linha do tempo do vídeo",
"volumeSlider": "Nível de volume",
"endsAt": "Termina às ${time}",
"pipActive": "Reproduzindo em Picture-in-Picture",
"pipFailed": "Falha ao iniciar picture-in-picture",
"pipErrors": {
"androidVersion": "Requer Android 8.0 ou superior",
"iosVersion": "Requer iOS 15.0 ou superior",
"permissionDisabled": "A permissão de picture-in-picture está desativada. Ative em Configurações > Apps > Plezy > Picture-in-picture",
"notSupported": "O dispositivo não suporta modo picture-in-picture",
"voSwitchFailed": "Falha ao trocar saída de vídeo para picture-in-picture",
"failed": "Falha ao iniciar picture-in-picture",
"unknown": "Ocorreu um erro: ${error}"
},
"chapters": "Capítulos",
"noChaptersAvailable": "Nenhum capítulo disponível",
"queue": "Fila",
"noQueueItems": "Nenhum item na fila",
"searchSubtitles": "Pesquisar legendas",
"language": "Idioma",
"noSubtitlesFound": "Nenhuma legenda encontrada",
"subtitleDownloaded": "Legenda baixada",
"subtitleDownloadFailed": "Falha ao baixar legenda",
"searchLanguages": "Pesquisar idiomas..."
},
"userStatus": {
"admin": "Admin",
"restricted": "Restrito",
"protected": "Protegido",
"current": "ATUAL"
},
"messages": {
"markedAsWatched": "Marcado como assistido",
"markedAsUnwatched": "Marcado como não assistido",
"markedAsWatchedOffline": "Marcado como assistido (será sincronizado quando online)",
"markedAsUnwatchedOffline": "Marcado como não assistido (será sincronizado quando online)",
"autoRemovedWatchedDownload": "Removido automaticamente: ${title}",
"removedFromContinueWatching": "Removido de Continuar Assistindo",
"errorLoading": "Erro: ${error}",
"fileInfoNotAvailable": "Informações do arquivo não disponíveis",
"errorLoadingFileInfo": "Erro ao carregar info do arquivo: ${error}",
"errorLoadingSeries": "Erro ao carregar série",
"errorLoadingSeason": "Erro ao carregar temporada",
"musicNotSupported": "Reprodução de música ainda não é suportada",
"logsCleared": "Logs limpos",
"logsCopied": "Logs copiados para a área de transferência",
"noLogsAvailable": "Nenhum log disponível",
"libraryScanning": "Escaneando \"${title}\"...",
"libraryScanStarted": "Escaneamento da biblioteca iniciado para \"${title}\"",
"libraryScanFailed": "Falha ao escanear biblioteca: ${error}",
"metadataRefreshing": "Atualizando metadados de \"${title}\"...",
"metadataRefreshStarted": "Atualização de metadados iniciada para \"${title}\"",
"metadataRefreshFailed": "Falha ao atualizar metadados: ${error}",
"logoutConfirm": "Tem certeza que deseja sair?",
"noSeasonsFound": "Nenhuma temporada encontrada",
"noEpisodesFound": "Nenhum episódio encontrado na primeira temporada",
"noEpisodesFoundGeneral": "Nenhum episódio encontrado",
"noResultsFound": "Nenhum resultado encontrado",
"sleepTimerSet": "Timer de sono definido para ${label}",
"noItemsAvailable": "Nenhum item disponível",
"failedToCreatePlayQueueNoItems": "Falha ao criar fila de reprodução - sem itens",
"failedPlayback": "Falha ao ${action}: ${error}",
"switchingToCompatiblePlayer": "Alternando para player compatível...",
"serverLimitTitle": "Falha na reprodução",
"serverLimitBody": "O servidor retornou um erro (HTTP 500). Isso geralmente significa que o proprietário do servidor definiu um limite de largura de banda ou transcodificação que está rejeitando sua sessão. Não há nada a ser feito pelo cliente — o proprietário do servidor precisa ajustar as configurações.",
"logsUploaded": "Logs enviados",
"logsUploadFailed": "Falha ao enviar logs",
"logId": "ID do Log"
},
"subtitlingStyling": {
"text": "Texto",
"border": "Borda",
"background": "Fundo",
"fontSize": "Tamanho da Fonte",
"textColor": "Cor do Texto",
"borderSize": "Tamanho da Borda",
"borderColor": "Cor da Borda",
"backgroundOpacity": "Opacidade do Fundo",
"backgroundColor": "Cor de Fundo",
"position": "Posição",
"assOverride": "Substituição ASS",
"bold": "Negrito",
"italic": "Itálico"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "Configurações avançadas do player de vídeo",
"presets": "Predefinições",
"noPresets": "Nenhuma predefinição salva",
"saveAsPreset": "Salvar como Predefinição...",
"presetName": "Nome da Predefinição",
"presetNameHint": "Insira um nome para esta predefinição",
"loadPreset": "Carregar",
"deletePreset": "Excluir",
"presetSaved": "Predefinição salva",
"presetLoaded": "Predefinição carregada",
"presetDeleted": "Predefinição excluída",
"confirmDeletePreset": "Tem certeza que deseja excluir esta predefinição?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Confirmar Ação"
},
"discover": {
"title": "Descobrir",
"switchProfile": "Trocar Perfil",
"noContentAvailable": "Nenhum conteúdo disponível",
"addMediaToLibraries": "Adicione mídias às suas bibliotecas",
"continueWatching": "Continuar Assistindo",
"playEpisode": "S${season}E${episode}",
"overview": "Sinopse",
"cast": "Elenco",
"extras": "Trailers e Extras",
"studio": "Estúdio",
"rating": "Avaliação",
"movie": "Filme",
"tvShow": "Série de TV",
"minutesLeft": "${minutes} min restantes"
},
"errors": {
"searchFailed": "Falha na busca: ${error}",
"connectionTimeout": "Tempo de conexão esgotado ao carregar ${context}",
"connectionFailed": "Não foi possível conectar ao servidor Plex",
"failedToLoad": "Falha ao carregar ${context}: ${error}",
"noClientAvailable": "Nenhum cliente disponível",
"authenticationFailed": "Falha na autenticação: ${error}",
"couldNotLaunchUrl": "Não foi possível abrir a URL de autenticação",
"pleaseEnterToken": "Insira um token",
"invalidToken": "Token inválido",
"failedToVerifyToken": "Falha ao verificar token: ${error}",
"failedToSwitchProfile": "Falha ao trocar para ${displayName}"
},
"libraries": {
"title": "Bibliotecas",
"scanLibraryFiles": "Escanear Arquivos da Biblioteca",
"scanLibrary": "Escanear Biblioteca",
"analyze": "Analisar",
"analyzeLibrary": "Analisar Biblioteca",
"refreshMetadata": "Atualizar Metadados",
"emptyTrash": "Esvaziar Lixeira",
"emptyingTrash": "Esvaziando lixeira de \"${title}\"...",
"trashEmptied": "Lixeira esvaziada de \"${title}\"",
"failedToEmptyTrash": "Falha ao esvaziar lixeira: ${error}",
"analyzing": "Analisando \"${title}\"...",
"analysisStarted": "Análise iniciada para \"${title}\"",
"failedToAnalyze": "Falha ao analisar biblioteca: ${error}",
"noLibrariesFound": "Nenhuma biblioteca encontrada",
"thisLibraryIsEmpty": "Esta biblioteca está vazia",
"all": "Todos",
"clearAll": "Limpar Tudo",
"scanLibraryConfirm": "Tem certeza que deseja escanear \"${title}\"?",
"analyzeLibraryConfirm": "Tem certeza que deseja analisar \"${title}\"?",
"refreshMetadataConfirm": "Tem certeza que deseja atualizar os metadados de \"${title}\"?",
"emptyTrashConfirm": "Tem certeza que deseja esvaziar a lixeira de \"${title}\"?",
"manageLibraries": "Gerenciar Bibliotecas",
"sort": "Ordenar",
"sortBy": "Ordenar Por",
"filters": "Filtros",
"confirmActionMessage": "Tem certeza que deseja realizar esta ação?",
"showLibrary": "Mostrar biblioteca",
"hideLibrary": "Ocultar biblioteca",
"libraryOptions": "Opções da biblioteca",
"content": "conteúdo da biblioteca",
"selectLibrary": "Selecionar biblioteca",
"filtersWithCount": "Filtros (${count})",
"noRecommendations": "Nenhuma recomendação disponível",
"noCollections": "Nenhuma coleção nesta biblioteca",
"noFoldersFound": "Nenhuma pasta encontrada",
"folders": "pastas",
"tabs": {
"recommended": "Recomendados",
"browse": "Navegar",
"collections": "Coleções",
"playlists": "Playlists"
},
"groupings": {
"title": "Agrupamento",
"all": "Todos",
"movies": "Filmes",
"shows": "Séries de TV",
"seasons": "Temporadas",
"episodes": "Episódios",
"folders": "Pastas"
}
},
"about": {
"title": "Sobre",
"openSourceLicenses": "Licenças Open Source",
"versionLabel": "Versão ${version}",
"appDescription": "Um belo cliente Plex para Flutter",
"viewLicensesDescription": "Ver licenças de bibliotecas de terceiros"
},
"serverSelection": {
"allServerConnectionsFailed": "Falha ao conectar a qualquer servidor. Verifique sua rede e tente novamente.",
"noServersFoundForAccount": "Nenhum servidor encontrado para ${username} (${email})",
"failedToLoadServers": "Falha ao carregar servidores: ${error}"
},
"hubDetail": {
"title": "Título",
"releaseYear": "Ano de Lançamento",
"dateAdded": "Data de Adição",
"rating": "Avaliação",
"noItemsFound": "Nenhum item encontrado"
},
"logs": {
"clearLogs": "Limpar Logs",
"copyLogs": "Copiar Logs",
"uploadLogs": "Enviar Logs"
},
"licenses": {
"relatedPackages": "Pacotes Relacionados",
"license": "Licença",
"licenseNumber": "Licença ${number}",
"licensesCount": "${count} licenças"
},
"navigation": {
"libraries": "Bibliotecas",
"downloads": "Downloads",
"liveTv": "TV ao Vivo"
},
"liveTv": {
"title": "TV ao Vivo",
"guide": "Guia",
"noChannels": "Nenhum canal disponível",
"noDvr": "Nenhum DVR configurado em nenhum servidor",
"noPrograms": "Nenhum dado de programação disponível",
"live": "AO VIVO",
"reloadGuide": "Recarregar Guia",
"now": "Agora",
"today": "Hoje",
"midnight": "Meia-noite",
"overnight": "Madrugada",
"morning": "Manhã",
"daytime": "Dia",
"evening": "Noite",
"lateNight": "Madrugada",
"whatsOn": "O que Está Passando",
"watchChannel": "Assistir Canal",
"favorites": "Favoritos",
"reorderFavorites": "Reordenar favoritos",
"joinSession": "Entrar na sessão em andamento",
"watchFromStart": "Assistir do início (${minutes} min atrás)",
"watchLive": "Assistir ao vivo",
"goToLive": "Ir para o ao vivo"
},
"collections": {
"title": "Coleções",
"collection": "Coleção",
"empty": "A coleção está vazia",
"unknownLibrarySection": "Não é possível excluir: Seção de biblioteca desconhecida",
"deleteCollection": "Excluir Coleção",
"deleteConfirm": "Tem certeza que deseja excluir \"${title}\"? Esta ação não pode ser desfeita.",
"deleted": "Coleção excluída",
"deleteFailed": "Falha ao excluir coleção",
"deleteFailedWithError": "Falha ao excluir coleção: ${error}",
"failedToLoadItems": "Falha ao carregar itens da coleção: ${error}",
"selectCollection": "Selecionar Coleção",
"collectionName": "Nome da Coleção",
"enterCollectionName": "Insira o nome da coleção",
"addedToCollection": "Adicionado à coleção",
"errorAddingToCollection": "Falha ao adicionar à coleção",
"created": "Coleção criada",
"removeFromCollection": "Remover da coleção",
"removeFromCollectionConfirm": "Remover \"${title}\" desta coleção?",
"removedFromCollection": "Removido da coleção",
"removeFromCollectionFailed": "Falha ao remover da coleção",
"removeFromCollectionError": "Erro ao remover da coleção: ${error}",
"searchCollections": "Pesquisar coleções..."
},
"playlists": {
"title": "Playlists",
"playlist": "Playlist",
"noPlaylists": "Nenhuma playlist encontrada",
"create": "Criar Playlist",
"playlistName": "Nome da Playlist",
"enterPlaylistName": "Insira o nome da playlist",
"delete": "Excluir Playlist",
"removeItem": "Remover da Playlist",
"smartPlaylist": "Playlist Inteligente",
"itemCount": "${count} itens",
"oneItem": "1 item",
"emptyPlaylist": "Esta playlist está vazia",
"deleteConfirm": "Excluir Playlist?",
"deleteMessage": "Tem certeza que deseja excluir \"${name}\"?",
"created": "Playlist criada",
"deleted": "Playlist excluída",
"itemAdded": "Adicionado à playlist",
"itemRemoved": "Removido da playlist",
"selectPlaylist": "Selecionar Playlist",
"errorCreating": "Falha ao criar playlist",
"errorDeleting": "Falha ao excluir playlist",
"errorLoading": "Falha ao carregar playlists",
"errorAdding": "Falha ao adicionar à playlist",
"errorReordering": "Falha ao reordenar item da playlist",
"errorRemoving": "Falha ao remover da playlist"
},
"watchTogether": {
"title": "Assistir Juntos",
"description": "Assista conteúdo sincronizado com amigos e família",
"createSession": "Criar Sessão",
"creating": "Criando...",
"joinSession": "Entrar na Sessão",
"joining": "Entrando...",
"controlMode": "Modo de Controle",
"controlModeQuestion": "Quem pode controlar a reprodução?",
"hostOnly": "Apenas o Anfitrião",
"anyone": "Qualquer pessoa",
"hostingSession": "Hospedando Sessão",
"inSession": "Em Sessão",
"sessionCode": "Código da Sessão",
"hostControlsPlayback": "Anfitrião controla a reprodução",
"anyoneCanControl": "Qualquer pessoa pode controlar a reprodução",
"hostControls": "Controle do anfitrião",
"anyoneControls": "Controle de todos",
"participants": "Participantes",
"host": "Anfitrião",
"hostBadge": "HOST",
"youAreHost": "Você é o anfitrião",
"watchingWithOthers": "Assistindo com outros",
"endSession": "Encerrar Sessão",
"leaveSession": "Sair da Sessão",
"endSessionQuestion": "Encerrar Sessão?",
"leaveSessionQuestion": "Sair da Sessão?",
"endSessionConfirm": "Isso encerrará a sessão para todos os participantes.",
"leaveSessionConfirm": "Você será removido da sessão.",
"endSessionConfirmOverlay": "Isso encerrará a sessão de visualização para todos os participantes.",
"leaveSessionConfirmOverlay": "Você será desconectado da sessão de visualização.",
"end": "Encerrar",
"leave": "Sair",
"syncing": "Sincronizando...",
"joinWatchSession": "Entrar na Sessão",
"enterCodeHint": "Insira o código de 5 caracteres",
"pasteFromClipboard": "Colar da área de transferência",
"pleaseEnterCode": "Insira um código de sessão",
"codeMustBe5Chars": "O código da sessão deve ter 5 caracteres",
"joinInstructions": "Insira o código da sessão compartilhado pelo anfitrião para entrar na sessão.",
"failedToCreate": "Falha ao criar sessão",
"failedToJoin": "Falha ao entrar na sessão",
"sessionCodeCopied": "Código da sessão copiado para a área de transferência",
"relayUnreachable": "O servidor de retransmissão está inacessível. Isso pode ser causado pelo seu provedor bloqueando a conexão. Você ainda pode tentar, mas o Assistir Juntos pode não funcionar.",
"reconnectingToHost": "Reconectando ao anfitrião...",
"currentPlayback": "Reprodução Atual",
"joinCurrentPlayback": "Entrar na Reprodução Atual",
"joinCurrentPlaybackDescription": "Voltar ao que o anfitrião está assistindo agora",
"failedToOpenCurrentPlayback": "Falha ao abrir reprodução atual",
"participantJoined": "${name} entrou",
"participantLeft": "${name} saiu",
"participantPaused": "${name} pausou",
"participantResumed": "${name} retomou",
"participantSeeked": "${name} avançou",
"participantBuffering": "${name} está carregando",
"waitingForParticipants": "Aguardando outros carregarem...",
"recentRooms": "Salas recentes",
"renameRoom": "Renomear sala",
"removeRoom": "Remover"
},
"downloads": {
"title": "Downloads",
"manage": "Gerenciar",
"tvShows": "Séries de TV",
"movies": "Filmes",
"noDownloads": "Nenhum download ainda",
"noDownloadsDescription": "Conteúdo baixado aparecerá aqui para visualização offline",
"downloadNow": "Baixar",
"deleteDownload": "Excluir download",
"retryDownload": "Tentar download novamente",
"downloadQueued": "Download na fila",
"serverErrorBitrate": "Erro do servidor — o arquivo pode exceder o limite de bitrate de streaming remoto",
"episodesQueued": "${count} episódios na fila de download",
"downloadDeleted": "Download excluído",
"deleteConfirm": "Tem certeza que deseja excluir \"${title}\"? Isso removerá o arquivo baixado do seu dispositivo.",
"deletingWithProgress": "Excluindo ${title}... (${current} de ${total})",
"noDownloadsTree": "Nenhum download",
"pauseAll": "Pausar todos",
"resumeAll": "Retomar todos",
"deleteAll": "Excluir todos",
"selectVersion": "Selecionar versão",
"allEpisodes": "Todos os episódios",
"unwatchedOnly": "Apenas não assistidos",
"nextNUnwatched": "Próximos ${count} não assistidos",
"customAmount": "Quantidade personalizada...",
"howManyEpisodes": "Quantos episódios?",
"itemsQueued": "${count} itens na fila de download",
"keepSynced": "Manter sincronizado",
"downloadOnce": "Baixar uma vez",
"keepNUnwatched": "Manter ${count} não assistidos",
"editSyncRule": "Editar regra de sincronização",
"removeSyncRule": "Remover regra de sincronização",
"removeSyncRuleConfirm": "Parar de sincronizar \"${title}\"? Os episódios baixados serão mantidos.",
"syncRuleCreated": "Regra de sincronização criada — mantendo ${count} episódios não assistidos",
"syncRuleUpdated": "Regra de sincronização atualizada",
"syncRuleRemoved": "Regra de sincronização removida",
"syncedNewEpisodes": "${count} novos episódios sincronizados para ${title}",
"activeSyncRules": "Regras de sincronização",
"noSyncRules": "Nenhuma regra de sincronização",
"manageSyncRule": "Gerenciar sincronização",
"editEpisodeCount": "Número de episódios",
"editSyncFilter": "Filtro de sincronização",
"syncAllItems": "Sincronizando todos os itens",
"syncUnwatchedItems": "Sincronizando itens não vistos",
"syncRuleListCreated": "Regra de sincronização criada"
},
"shaders": {
"title": "Shaders",
"noShaderDescription": "Sem aprimoramento de vídeo",
"nvscalerDescription": "Escalonamento de imagem NVIDIA para vídeo mais nítido",
"qualityFast": "Rápido",
"qualityHQ": "Alta Qualidade",
"mode": "Modo",
"importShader": "Importar Shader",
"customShaderDescription": "Shader GLSL personalizado",
"shaderImported": "Shader importado",
"shaderImportFailed": "Falha ao importar shader",
"deleteShader": "Excluir Shader",
"deleteShaderConfirm": "Excluir \"${name}\"?"
},
"companionRemote": {
"title": "Controle Remoto",
"connectedTo": "Conectado a ${name}",
"session": {
"startingServer": "A iniciar servidor remoto...",
"failedToCreate": "Falha ao iniciar o servidor remoto:",
"hostAddress": "Endereço do host",
"connected": "Conectado",
"serverRunning": "Servidor remoto ativo",
"serverStopped": "Servidor remoto parado",
"serverRunningDescription": "Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação",
"serverStoppedDescription": "Inicie o servidor para permitir que dispositivos móveis se conectem",
"usePhoneToControl": "Use o seu dispositivo móvel para controlar esta aplicação",
"startServer": "Iniciar servidor",
"stopServer": "Parar servidor",
"minimize": "Minimizar"
},
"pairing": {
"discoveryDescription": "Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "A conectar...",
"searchingForDevices": "A procurar dispositivos...",
"noDevicesFound": "Nenhum dispositivo encontrado na sua rede",
"noDevicesHint": "Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi",
"availableDevices": "Dispositivos disponíveis",
"manualConnection": "Conexão manual",
"cryptoInitFailed": "Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.",
"validationHostRequired": "Introduza o endereço do host",
"validationHostFormat": "O formato deve ser IP:porta (ex. 192.168.1.100:48632)",
"connectionTimedOut": "Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.",
"sessionNotFound": "Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.",
"authFailed": "Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.",
"failedToConnect": "Falha ao conectar: ${error}"
},
"remote": {
"disconnectConfirm": "Deseja desconectar da sessão remota?",
"reconnecting": "Reconectando...",
"attemptOf": "Tentativa ${current} de 5",
"retryNow": "Tentar Agora",
"tabRemote": "Remoto",
"tabPlay": "Reproduzir",
"tabMore": "Mais",
"menu": "Menu",
"tabNavigation": "Navegação",
"tabDiscover": "Descobrir",
"tabLibraries": "Bibliotecas",
"tabSearch": "Buscar",
"tabDownloads": "Downloads",
"tabSettings": "Configurações",
"previous": "Anterior",
"playPause": "Reproduzir/Pausar",
"next": "Próximo",
"seekBack": "Retroceder",
"stop": "Parar",
"seekForward": "Avançar",
"volume": "Volume",
"volumeDown": "Diminuir",
"volumeUp": "Aumentar",
"fullscreen": "Tela Cheia",
"subtitles": "Legendas",
"audio": "Áudio",
"searchHint": "Buscar no desktop..."
}
},
"videoSettings": {
"playbackSettings": "Configurações de Reprodução",
"playbackSpeed": "Velocidade de Reprodução",
"sleepTimer": "Timer de Sono",
"audioSync": "Sincronia de Áudio",
"subtitleSync": "Sincronia de Legendas",
"hdr": "HDR",
"audioOutput": "Saída de Áudio",
"performanceOverlay": "Overlay de Desempenho",
"audioPassthrough": "Passagem de Áudio",
"audioNormalization": "Normalização de Áudio"
},
"externalPlayer": {
"title": "Player Externo",
"useExternalPlayer": "Usar Player Externo",
"useExternalPlayerDescription": "Abrir vídeos em um app externo em vez do player integrado",
"selectPlayer": "Selecionar Player",
"customPlayers": "Players Personalizados",
"systemDefault": "Padrão do Sistema",
"addCustomPlayer": "Adicionar Player Personalizado",
"playerName": "Nome do Player",
"playerCommand": "Comando",
"playerPackage": "Nome do Pacote",
"playerUrlScheme": "Esquema de URL",
"off": "Desativado",
"launchFailed": "Falha ao abrir player externo",
"appNotInstalled": "${name} não está instalado",
"playInExternalPlayer": "Reproduzir no Player Externo"
},
"metadataEdit": {
"editMetadata": "Editar...",
"screenTitle": "Editar Metadados",
"basicInfo": "Informações Básicas",
"artwork": "Arte",
"advancedSettings": "Configurações Avançadas",
"title": "Título",
"sortTitle": "Título para Ordenação",
"originalTitle": "Título Original",
"releaseDate": "Data de Lançamento",
"contentRating": "Classificação Indicativa",
"studio": "Estúdio",
"tagline": "Tagline",
"summary": "Sinopse",
"poster": "Poster",
"background": "Plano de Fundo",
"logo": "Logo",
"squareArt": "Imagem Quadrada",
"selectPoster": "Selecionar Poster",
"selectBackground": "Selecionar Plano de Fundo",
"selectLogo": "Selecionar Logo",
"selectSquareArt": "Selecionar Imagem Quadrada",
"fromUrl": "Da URL",
"uploadFile": "Enviar Arquivo",
"enterImageUrl": "Insira a URL da imagem",
"imageUrl": "URL da Imagem",
"metadataUpdated": "Metadados atualizados",
"metadataUpdateFailed": "Falha ao atualizar metadados",
"artworkUpdated": "Arte atualizada",
"artworkUpdateFailed": "Falha ao atualizar arte",
"noArtworkAvailable": "Nenhuma arte disponível",
"notSet": "Não definido",
"libraryDefault": "Padrão da biblioteca",
"accountDefault": "Padrão da conta",
"seriesDefault": "Padrão da série",
"episodeSorting": "Ordenação de Episódios",
"oldestFirst": "Mais antigos primeiro",
"newestFirst": "Mais recentes primeiro",
"keep": "Manter",
"allEpisodes": "Todos os episódios",
"latestEpisodes": "${count} episódios mais recentes",
"latestEpisode": "Episódio mais recente",
"episodesAddedPastDays": "Episódios adicionados nos últimos ${count} dias",
"deleteAfterPlaying": "Excluir Episódios Após Reproduzir",
"never": "Nunca",
"afterADay": "Após um dia",
"afterAWeek": "Após uma semana",
"afterAMonth": "Após um mês",
"onNextRefresh": "Na próxima atualização",
"seasons": "Temporadas",
"show": "Mostrar",
"hide": "Ocultar",
"episodeOrdering": "Ordenação de Episódios",
"tmdbAiring": "The Movie Database (Exibição)",
"tvdbAiring": "TheTVDB (Exibição)",
"tvdbAbsolute": "TheTVDB (Absoluto)",
"metadataLanguage": "Idioma dos Metadados",
"useOriginalTitle": "Usar Título Original",
"preferredAudioLanguage": "Idioma de Áudio Preferido",
"preferredSubtitleLanguage": "Idioma de Legenda Preferido",
"subtitleMode": "Modo de Seleção Automática de Legendas",
"manuallySelected": "Seleção manual",
"shownWithForeignAudio": "Exibir com áudio estrangeiro",
"alwaysEnabled": "Sempre ativado",
"tags": "Tags",
"addTag": "Adicionar tag",
"genre": "Gênero",
"director": "Diretor",
"writer": "Roteirista",
"producer": "Produtor",
"country": "País",
"collection": "Coleção",
"label": "Rótulo",
"style": "Estilo",
"mood": "Humor"
},
"serverTasks": {
"title": "Tarefas do servidor",
"failedToLoad": "Falha ao carregar tarefas",
"noTasks": "Nenhuma tarefa em execução"
},
"trakt": {
"title": "Trakt",
"connected": "Conectado",
"connectedAs": "Conectado como @${username}",
"disconnectConfirm": "Desconectar conta do Trakt?",
"disconnectConfirmBody": "O Plezy deixará de enviar eventos de reprodução ao Trakt. Você pode reconectar a qualquer momento.",
"scrobble": "Scrobbling em tempo real",
"scrobbleDescription": "Envia eventos de reprodução, pausa e parada ao Trakt durante a exibição.",
"watchedSync": "Sincronizar status de assistido",
"watchedSyncDescription": "Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt.",
"deviceCodeTitle": "Ativar o Plezy no Trakt",
"deviceCodeBody": "Acesse ${url} e digite este código:",
"openTraktActivate": "Abrir o Trakt para ativar",
"waitingForAuthorization": "Aguardando autorização…",
"codeCopied": "Código copiado",
"connectFailed": "Não foi possível conectar ao Trakt. Tente novamente."
}
}
+978
View File
@@ -0,0 +1,978 @@
{
"app": {
"title": "Plezy"
},
"auth": {
"signInWithPlex": "Войти через Plex",
"showQRCode": "Показать QR-код",
"authenticate": "Аутентификация",
"authenticationTimeout": "Время аутентификации истекло. Попробуйте снова.",
"scanQRToSignIn": "Отсканируйте QR-код для входа",
"waitingForAuth": "Ожидание аутентификации...\nЗавершите вход в браузере.",
"useBrowser": "Использовать браузер"
},
"common": {
"cancel": "Отмена",
"save": "Сохранить",
"close": "Закрыть",
"clear": "Очистить",
"reset": "Сбросить",
"later": "Позже",
"submit": "Отправить",
"confirm": "Подтвердить",
"retry": "Повторить",
"logout": "Выйти",
"unknown": "Неизвестно",
"refresh": "Обновить",
"yes": "Да",
"no": "Нет",
"delete": "Удалить",
"shuffle": "Перемешать",
"addTo": "Добавить в...",
"createNew": "Создать новый",
"connect": "Подключить",
"disconnect": "Отключить",
"play": "Воспроизвести",
"pause": "Пауза",
"resume": "Продолжить",
"error": "Ошибка",
"search": "Поиск",
"home": "Главная",
"back": "Назад",
"settings": "Настройки",
"mute": "Без звука",
"ok": "OK",
"reconnect": "Переподключить",
"exitConfirmTitle": "Выйти из приложения?",
"exitConfirmMessage": "Вы уверены, что хотите выйти?",
"dontAskAgain": "Больше не спрашивать",
"exit": "Выход",
"viewAll": "Показать все",
"checkingNetwork": "Проверка сети...",
"refreshingServers": "Обновление серверов...",
"loadingServers": "Загрузка серверов...",
"connectingToServers": "Подключение к серверам...",
"startingOfflineMode": "Запуск автономного режима...",
"loading": "Загрузка..."
},
"screens": {
"licenses": "Лицензии",
"switchProfile": "Сменить профиль",
"subtitleStyling": "Стиль субтитров",
"mpvConfig": "mpv.conf",
"logs": "Логи"
},
"update": {
"available": "Доступно обновление",
"versionAvailable": "Доступна версия ${version}",
"currentVersion": "Текущая: ${version}",
"skipVersion": "Пропустить эту версию",
"viewRelease": "Посмотреть релиз",
"latestVersion": "У вас последняя версия",
"checkFailed": "Не удалось проверить обновления"
},
"settings": {
"title": "Настройки",
"supportDeveloper": "Поддержать Plezy",
"supportDeveloperDescription": "Пожертвуйте через Liberapay на развитие",
"language": "Язык",
"theme": "Тема",
"appearance": "Внешний вид",
"videoPlayback": "Воспроизведение видео",
"videoPlaybackDescription": "Настройка поведения воспроизведения",
"advanced": "Дополнительно",
"episodePosterMode": "Стиль постера эпизода",
"seriesPoster": "Постер сериала",
"seasonPoster": "Постер сезона",
"episodeThumbnail": "Миниатюра",
"showHeroSectionDescription": "Показывать карусель избранного контента на главном экране",
"secondsLabel": "Секунды",
"minutesLabel": "Минуты",
"secondsShort": "с",
"minutesShort": "м",
"durationHint": "Введите длительность (${min}-${max})",
"systemTheme": "Системная",
"lightTheme": "Светлая",
"darkTheme": "Тёмная",
"oledTheme": "OLED",
"libraryDensity": "Плотность библиотеки",
"compact": "Компактный",
"comfortable": "Комфортный",
"viewMode": "Режим просмотра",
"gridView": "Сетка",
"listView": "Список",
"showHeroSection": "Показать раздел избранного",
"useGlobalHubs": "Использовать макет Plex Home",
"useGlobalHubsDescription": "Показывать хабы главной страницы как в официальном клиенте Plex. При выключении показывает рекомендации по библиотекам.",
"showServerNameOnHubs": "Показывать имя сервера в хабах",
"showServerNameOnHubsDescription": "Всегда показывать имя сервера в заголовках хабов. При выключении показывает только для дублирующихся имён.",
"alwaysKeepSidebarOpen": "Всегда держать боковую панель открытой",
"alwaysKeepSidebarOpenDescription": "Боковая панель остаётся развёрнутой, область контента подстраивается",
"showUnwatchedCount": "Показывать количество непросмотренных",
"showUnwatchedCountDescription": "Отображать количество непросмотренных эпизодов для сериалов и сезонов",
"hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов",
"hideSpoilersDescription": "Размывать миниатюры и скрывать описания эпизодов, которые вы ещё не смотрели",
"playerBackend": "Бэкенд плеера",
"exoPlayer": "ExoPlayer (Рекомендуется)",
"mpv": "mpv",
"hardwareDecoding": "Аппаратное декодирование",
"hardwareDecodingDescription": "Использовать аппаратное ускорение, когда доступно",
"bufferSize": "Размер буфера",
"bufferSizeMB": "${size}МБ",
"bufferSizeAuto": "Авто (Рекомендуется)",
"bufferSizeWarning": "У вашего устройства ${heap}МБ памяти. Буфер ${size}МБ может вызвать проблемы с воспроизведением.",
"subtitleStyling": "Стиль субтитров",
"subtitleStylingDescription": "Настроить внешний вид субтитров",
"smallSkipDuration": "Малая перемотка",
"largeSkipDuration": "Большая перемотка",
"rewindOnResume": "Перемотка при возобновлении",
"secondsUnit": "${seconds} секунд",
"defaultSleepTimer": "Таймер сна по умолчанию",
"minutesUnit": "${minutes} минут",
"rememberTrackSelections": "Запоминать выбор дорожек для каждого сериала/фильма",
"rememberTrackSelectionsDescription": "Автоматически сохранять предпочтения языка аудио и субтитров при переключении дорожек во время воспроизведения",
"clickVideoTogglesPlayback": "Клик по видео для переключения воспроизведения/паузы",
"clickVideoTogglesPlaybackDescription": "Если включено, клик по видеоплееру воспроизводит/ставит на паузу. В противном случае показывает/скрывает элементы управления.",
"videoPlayerControls": "Элементы управления плеером",
"keyboardShortcuts": "Горячие клавиши",
"keyboardShortcutsDescription": "Настроить горячие клавиши",
"videoPlayerNavigation": "Навигация видеоплеера",
"videoPlayerNavigationDescription": "Использовать клавиши стрелок для навигации по элементам управления плеером",
"watchTogetherRelay": "Relay совместного просмотра",
"watchTogetherRelayDescription": "Указать пользовательский relay-сервер для совместного просмотра. Все участники должны использовать один и тот же сервер.",
"watchTogetherRelayHint": "https://my-relay.example.com",
"crashReporting": "Отчёты об ошибках",
"crashReportingDescription": "Отправлять отчёты об ошибках для улучшения приложения",
"debugLogging": "Журнал отладки",
"debugLoggingDescription": "Включить подробное журналирование для устранения неполадок",
"viewLogs": "Просмотр логов",
"viewLogsDescription": "Просмотр логов приложения",
"clearCache": "Очистить кэш",
"clearCacheDescription": "Это удалит все кэшированные изображения и данные. После очистки кэша приложение может загружать контент дольше.",
"clearCacheSuccess": "Кэш успешно очищен",
"resetSettings": "Сбросить настройки",
"resetSettingsDescription": "Все настройки будут сброшены до значений по умолчанию. Это действие нельзя отменить.",
"resetSettingsSuccess": "Настройки успешно сброшены",
"shortcutsReset": "Горячие клавиши сброшены по умолчанию",
"about": "О приложении",
"aboutDescription": "Информация о приложении и лицензии",
"updates": "Обновления",
"updateAvailable": "Доступно обновление",
"checkForUpdates": "Проверить обновления",
"validationErrorEnterNumber": "Введите корректное число",
"validationErrorDuration": "Длительность должна быть от ${min} до ${max} ${unit}",
"shortcutAlreadyAssigned": "Клавиша уже назначена для ${action}",
"shortcutUpdated": "Клавиша обновлена для ${action}",
"autoSkip": "Автопропуск",
"autoSkipIntro": "Автопропуск вступления",
"autoSkipIntroDescription": "Автоматически пропускать маркеры вступления через несколько секунд",
"autoSkipCredits": "Автопропуск титров",
"autoSkipCreditsDescription": "Автоматически пропускать титры и воспроизводить следующий эпизод",
"autoSkipDelay": "Задержка автопропуска",
"autoSkipDelayDescription": "Подождать ${seconds} секунд перед автопропуском",
"introPattern": "Шаблон маркера вступления",
"introPatternDescription": "Регулярное выражение для распознавания маркеров вступления в заголовках глав",
"creditsPattern": "Шаблон маркера титров",
"creditsPatternDescription": "Регулярное выражение для распознавания маркеров титров в заголовках глав",
"invalidRegex": "Недопустимое регулярное выражение",
"downloads": "Загрузки",
"downloadLocationDescription": "Выберите место для хранения загруженного контента",
"downloadLocationDefault": "По умолчанию (Хранилище приложения)",
"downloadLocationCustom": "Другое расположение",
"selectFolder": "Выбрать папку",
"resetToDefault": "Сбросить по умолчанию",
"currentPath": "Текущий: ${path}",
"downloadLocationChanged": "Место загрузки изменено",
"downloadLocationReset": "Место загрузки сброшено по умолчанию",
"downloadLocationInvalid": "Выбранная папка недоступна для записи",
"downloadLocationSelectError": "Не удалось выбрать папку",
"downloadOnWifiOnly": "Загружать только по WiFi",
"downloadOnWifiOnlyDescription": "Запретить загрузку по мобильным данным",
"autoRemoveWatchedDownloads": "Автоудаление просмотренных загрузок",
"autoRemoveWatchedDownloadsDescription": "Автоматически удалять загруженные эпизоды и фильмы после просмотра",
"cellularDownloadBlocked": "Загрузка по мобильным данным отключена. Подключитесь к WiFi или измените настройку.",
"maxVolume": "Максимальная громкость",
"maxVolumeDescription": "Разрешить усиление громкости выше 100% для тихих медиа",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Показывать, что вы смотрите, в Discord",
"trakt": "Trakt",
"traktDescription": "Синхронизировать историю просмотров с Trakt",
"companionRemoteServer": "Сервер удалённого управления",
"companionRemoteServerDescription": "Разрешить мобильным устройствам в сети управлять этим приложением",
"autoPip": "Автоматический «картинка в картинке»",
"autoPipDescription": "Автоматически переходить в режим «картинка в картинке» при выходе из приложения во время воспроизведения",
"matchContentFrameRate": "Соответствие частоты кадров контента",
"matchContentFrameRateDescription": "Настроить частоту обновления дисплея под видеоконтент, уменьшая дрожание и экономя батарею",
"matchRefreshRate": "Соответствие частоты обновления",
"matchRefreshRateDescription": "Переключать частоту обновления дисплея под видеоконтент в полноэкранном режиме",
"matchDynamicRange": "Соответствие динамического диапазона",
"matchDynamicRangeDescription": "Автоматически включать HDR для HDR-контента и возвращать SDR при выходе из плеера",
"displaySwitchDelay": "Задержка переключения дисплея",
"tunneledPlayback": "Туннельное воспроизведение",
"tunneledPlaybackDescription": "Использовать аппаратный видеотуннелинг. Отключите, если видите чёрный экран со звуком при HDR-контенте",
"requireProfileSelectionOnOpen": "Запрашивать профиль при запуске",
"requireProfileSelectionOnOpenDescription": "Показывать выбор профиля при каждом открытии приложения",
"confirmExitOnBack": "Подтверждать выход",
"confirmExitOnBackDescription": "Показывать диалог подтверждения при нажатии «назад» для выхода из приложения",
"forceTvMode": "Принудительный режим ТВ",
"forceTvModeDescription": "Использовать макет ТВ независимо от автоматического определения. Полезно на устройствах Android TV, которые не сообщают о функции leanback. Приложение перезапустится при изменении.",
"autoHidePerformanceOverlay": "Автоскрытие оверлея производительности",
"autoHidePerformanceOverlayDescription": "Скрывать оверлей производительности вместе с элементами управления воспроизведением",
"showNavBarLabels": "Показывать подписи панели навигации",
"showNavBarLabelsDescription": "Отображать текстовые подписи под иконками панели навигации",
"liveTvDefaultFavorites": "Избранные каналы по умолчанию",
"liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Поиск фильмов, сериалов, музыки...",
"tryDifferentTerm": "Попробуйте другой запрос",
"searchYourMedia": "Поиск в вашей медиатеке",
"enterTitleActorOrKeyword": "Введите название, актёра или ключевое слово"
},
"hotkeys": {
"setShortcutFor": "Назначить клавишу для ${actionName}",
"clearShortcut": "Очистить клавишу",
"actions": {
"playPause": "Воспроизведение/Пауза",
"volumeUp": "Громкость выше",
"volumeDown": "Громкость ниже",
"seekForward": "Перемотка вперёд (${seconds}с)",
"seekBackward": "Перемотка назад (${seconds}с)",
"fullscreenToggle": "Полноэкранный режим",
"muteToggle": "Вкл./выкл. звук",
"subtitleToggle": "Вкл./выкл. субтитры",
"audioTrackNext": "Следующая аудиодорожка",
"subtitleTrackNext": "Следующая дорожка субтитров",
"chapterNext": "Следующая глава",
"chapterPrevious": "Предыдущая глава",
"speedIncrease": "Увеличить скорость",
"speedDecrease": "Уменьшить скорость",
"speedReset": "Сбросить скорость",
"subSeekNext": "К следующему субтитру",
"subSeekPrev": "К предыдущему субтитру",
"shaderToggle": "Вкл./выкл. шейдеры",
"skipMarker": "Пропустить вступление/титры"
}
},
"fileInfo": {
"title": "Информация о файле",
"video": "Видео",
"audio": "Аудио",
"file": "Файл",
"advanced": "Дополнительно",
"codec": "Кодек",
"resolution": "Разрешение",
"bitrate": "Битрейт",
"frameRate": "Частота кадров",
"aspectRatio": "Соотношение сторон",
"profile": "Профиль",
"bitDepth": "Глубина цвета",
"colorSpace": "Цветовое пространство",
"colorRange": "Цветовой диапазон",
"colorPrimaries": "Цветовые первичные",
"chromaSubsampling": "Субдискретизация цветности",
"channels": "Каналы",
"subtitles": "Субтитры",
"overallBitrate": "Общий битрейт",
"path": "Путь",
"size": "Размер",
"container": "Контейнер",
"duration": "Длительность",
"optimizedForStreaming": "Оптимизировано для стриминга",
"has64bitOffsets": "64-битные смещения"
},
"mediaMenu": {
"markAsWatched": "Отметить как просмотренное",
"markAsUnwatched": "Отметить как непросмотренное",
"removeFromContinueWatching": "Удалить из «Продолжить просмотр»",
"goToSeries": "Перейти к сериалу",
"goToSeason": "Перейти к сезону",
"shufflePlay": "Случайное воспроизведение",
"fileInfo": "Информация о файле",
"deleteFromServer": "Удалить с сервера",
"confirmDelete": "Это навсегда удалит этот медиафайл и его файлы с вашего сервера. Это действие нельзя отменить.",
"deleteMultipleWarning": "Это включает все эпизоды и их файлы.",
"mediaDeletedSuccessfully": "Медиаэлемент успешно удалён",
"mediaFailedToDelete": "Не удалось удалить медиаэлемент",
"rate": "Оценить",
"playFromBeginning": "Воспроизвести сначала",
"playVersion": "Воспроизвести версию..."
},
"accessibility": {
"mediaCardMovie": "${title}, фильм",
"mediaCardShow": "${title}, сериал",
"mediaCardEpisode": "${title}, ${episodeInfo}",
"mediaCardSeason": "${title}, ${seasonInfo}",
"mediaCardWatched": "просмотрено",
"mediaCardPartiallyWatched": "просмотрено ${percent} процентов",
"mediaCardUnwatched": "не просмотрено",
"tapToPlay": "Нажмите для воспроизведения"
},
"tooltips": {
"shufflePlay": "Случайное воспроизведение",
"playTrailer": "Воспроизвести трейлер",
"markAsWatched": "Отметить как просмотренное",
"markAsUnwatched": "Отметить как непросмотренное"
},
"videoControls": {
"audioLabel": "Аудио",
"subtitlesLabel": "Субтитры",
"resetToZero": "Сбросить до 0мс",
"addTime": "+${amount}${unit}",
"minusTime": "-${amount}${unit}",
"playsLater": "${label} воспроизводится позже",
"playsEarlier": "${label} воспроизводится раньше",
"noOffset": "Без смещения",
"letterbox": "Letterbox",
"fillScreen": "Заполнить экран",
"stretch": "Растянуть",
"lockRotation": "Заблокировать поворот",
"unlockRotation": "Разблокировать поворот",
"timerActive": "Таймер активен",
"playbackWillPauseIn": "Воспроизведение будет приостановлено через ${duration}",
"stillWatching": "Всё ещё смотрите?",
"pausingIn": "Пауза через ${seconds}с",
"continueWatching": "Продолжить",
"autoPlayNext": "Автовоспроизведение следующего",
"playNext": "Следующее",
"playButton": "Воспроизвести",
"pauseButton": "Пауза",
"seekBackwardButton": "Перемотка назад на ${seconds} секунд",
"seekForwardButton": "Перемотка вперёд на ${seconds} секунд",
"previousButton": "Предыдущий эпизод",
"nextButton": "Следующий эпизод",
"previousChapterButton": "Предыдущая глава",
"nextChapterButton": "Следующая глава",
"muteButton": "Без звука",
"unmuteButton": "Включить звук",
"settingsButton": "Настройки видео",
"tracksButton": "Аудио и субтитры",
"chaptersButton": "Главы",
"versionsButton": "Версии видео",
"pipButton": "Режим «картинка в картинке»",
"aspectRatioButton": "Соотношение сторон",
"ambientLighting": "Фоновая подсветка",
"fullscreenButton": "Полноэкранный режим",
"exitFullscreenButton": "Выйти из полноэкранного режима",
"alwaysOnTopButton": "Всегда поверх",
"rotationLockButton": "Блокировка поворота",
"lockScreen": "Заблокировать экран",
"screenLockButton": "Блокировка экрана",
"longPressToUnlock": "Удерживайте для разблокировки",
"timelineSlider": "Временная шкала",
"volumeSlider": "Уровень громкости",
"endsAt": "Закончится в ${time}",
"pipActive": "Воспроизводится в режиме «картинка в картинке»",
"pipFailed": "Не удалось запустить режим «картинка в картинке»",
"pipErrors": {
"androidVersion": "Требуется Android 8.0 или новее",
"iosVersion": "Требуется iOS 15.0 или новее",
"permissionDisabled": "Разрешение «картинка в картинке» отключено. Включите в Настройки > Приложения > Plezy > Картинка в картинке",
"notSupported": "Устройство не поддерживает режим «картинка в картинке»",
"voSwitchFailed": "Не удалось переключить видеовыход для «картинки в картинке»",
"failed": "Не удалось запустить режим «картинка в картинке»",
"unknown": "Произошла ошибка: ${error}"
},
"chapters": "Главы",
"noChaptersAvailable": "Главы недоступны",
"queue": "Очередь",
"noQueueItems": "В очереди нет элементов",
"searchSubtitles": "Поиск субтитров",
"language": "Язык",
"noSubtitlesFound": "Субтитры не найдены",
"subtitleDownloaded": "Субтитры загружены",
"subtitleDownloadFailed": "Не удалось загрузить субтитры",
"searchLanguages": "Поиск языков..."
},
"userStatus": {
"admin": "Администратор",
"restricted": "Ограниченный",
"protected": "Защищённый",
"current": "ТЕКУЩИЙ"
},
"messages": {
"markedAsWatched": "Отмечено как просмотренное",
"markedAsUnwatched": "Отмечено как непросмотренное",
"markedAsWatchedOffline": "Отмечено как просмотренное (синхронизируется при подключении)",
"markedAsUnwatchedOffline": "Отмечено как непросмотренное (синхронизируется при подключении)",
"autoRemovedWatchedDownload": "Автоудалено: ${title}",
"removedFromContinueWatching": "Удалено из «Продолжить просмотр»",
"errorLoading": "Ошибка: ${error}",
"fileInfoNotAvailable": "Информация о файле недоступна",
"errorLoadingFileInfo": "Ошибка загрузки информации о файле: ${error}",
"errorLoadingSeries": "Ошибка загрузки сериала",
"errorLoadingSeason": "Ошибка загрузки сезона",
"musicNotSupported": "Воспроизведение музыки пока не поддерживается",
"logsCleared": "Логи очищены",
"logsCopied": "Логи скопированы в буфер обмена",
"noLogsAvailable": "Логи отсутствуют",
"libraryScanning": "Сканирование \"${title}\"...",
"libraryScanStarted": "Сканирование библиотеки начато для \"${title}\"",
"libraryScanFailed": "Не удалось отсканировать библиотеку: ${error}",
"metadataRefreshing": "Обновление метаданных \"${title}\"...",
"metadataRefreshStarted": "Обновление метаданных начато для \"${title}\"",
"metadataRefreshFailed": "Не удалось обновить метаданные: ${error}",
"logoutConfirm": "Вы уверены, что хотите выйти?",
"noSeasonsFound": "Сезоны не найдены",
"noEpisodesFound": "Эпизоды в первом сезоне не найдены",
"noEpisodesFoundGeneral": "Эпизоды не найдены",
"noResultsFound": "Результаты не найдены",
"sleepTimerSet": "Таймер сна установлен на ${label}",
"noItemsAvailable": "Нет доступных элементов",
"failedToCreatePlayQueueNoItems": "Не удалось создать очередь воспроизведения — нет элементов",
"failedPlayback": "Не удалось ${action}: ${error}",
"switchingToCompatiblePlayer": "Переключение на совместимый плеер...",
"serverLimitTitle": "Ошибка воспроизведения",
"serverLimitBody": "Сервер вернул ошибку (HTTP 500). Обычно это означает, что владелец сервера установил лимит пропускной способности или транскодирования, из-за которого ваш сеанс отклоняется. Со стороны клиента сделать ничего нельзя — владельцу сервера нужно изменить настройки.",
"logsUploaded": "Логи загружены",
"logsUploadFailed": "Не удалось загрузить логи",
"logId": "ID лога"
},
"subtitlingStyling": {
"text": "Текст",
"border": "Обводка",
"background": "Фон",
"fontSize": "Размер шрифта",
"textColor": "Цвет текста",
"borderSize": "Размер обводки",
"borderColor": "Цвет обводки",
"backgroundOpacity": "Прозрачность фона",
"backgroundColor": "Цвет фона",
"position": "Позиция",
"assOverride": "Переопределение ASS",
"bold": "Жирный",
"italic": "Курсив"
},
"mpvConfig": {
"title": "mpv.conf",
"description": "Расширенные настройки видеоплеера",
"presets": "Пресеты",
"noPresets": "Нет сохранённых пресетов",
"saveAsPreset": "Сохранить как пресет...",
"presetName": "Название пресета",
"presetNameHint": "Введите название для пресета",
"loadPreset": "Загрузить",
"deletePreset": "Удалить",
"presetSaved": "Пресет сохранён",
"presetLoaded": "Пресет загружен",
"presetDeleted": "Пресет удалён",
"confirmDeletePreset": "Вы уверены, что хотите удалить этот пресет?",
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Подтвердить действие"
},
"discover": {
"title": "Обзор",
"switchProfile": "Сменить профиль",
"noContentAvailable": "Контент недоступен",
"addMediaToLibraries": "Добавьте медиафайлы в ваши библиотеки",
"continueWatching": "Продолжить просмотр",
"playEpisode": "S${season}E${episode}",
"overview": "Обзор",
"cast": "В ролях",
"extras": "Трейлеры и доп. материалы",
"studio": "Студия",
"rating": "Рейтинг",
"movie": "Фильм",
"tvShow": "Сериал",
"minutesLeft": "Осталось ${minutes} мин"
},
"errors": {
"searchFailed": "Ошибка поиска: ${error}",
"connectionTimeout": "Таймаут подключения при загрузке ${context}",
"connectionFailed": "Не удаётся подключиться к серверу Plex",
"failedToLoad": "Не удалось загрузить ${context}: ${error}",
"noClientAvailable": "Клиент недоступен",
"authenticationFailed": "Ошибка аутентификации: ${error}",
"couldNotLaunchUrl": "Не удалось открыть URL аутентификации",
"pleaseEnterToken": "Введите токен",
"invalidToken": "Недействительный токен",
"failedToVerifyToken": "Не удалось проверить токен: ${error}",
"failedToSwitchProfile": "Не удалось переключиться на ${displayName}"
},
"libraries": {
"title": "Библиотеки",
"scanLibraryFiles": "Сканировать файлы библиотеки",
"scanLibrary": "Сканировать библиотеку",
"analyze": "Анализировать",
"analyzeLibrary": "Анализировать библиотеку",
"refreshMetadata": "Обновить метаданные",
"emptyTrash": "Очистить корзину",
"emptyingTrash": "Очистка корзины для \"${title}\"...",
"trashEmptied": "Корзина очищена для \"${title}\"",
"failedToEmptyTrash": "Не удалось очистить корзину: ${error}",
"analyzing": "Анализ \"${title}\"...",
"analysisStarted": "Анализ начат для \"${title}\"",
"failedToAnalyze": "Не удалось проанализировать библиотеку: ${error}",
"noLibrariesFound": "Библиотеки не найдены",
"thisLibraryIsEmpty": "Эта библиотека пуста",
"all": "Все",
"clearAll": "Очистить все",
"scanLibraryConfirm": "Вы уверены, что хотите сканировать \"${title}\"?",
"analyzeLibraryConfirm": "Вы уверены, что хотите проанализировать \"${title}\"?",
"refreshMetadataConfirm": "Вы уверены, что хотите обновить метаданные для \"${title}\"?",
"emptyTrashConfirm": "Вы уверены, что хотите очистить корзину для \"${title}\"?",
"manageLibraries": "Управление библиотеками",
"sort": "Сортировка",
"sortBy": "Сортировать по",
"filters": "Фильтры",
"confirmActionMessage": "Вы уверены, что хотите выполнить это действие?",
"showLibrary": "Показать библиотеку",
"hideLibrary": "Скрыть библиотеку",
"libraryOptions": "Параметры библиотеки",
"content": "содержимое библиотеки",
"selectLibrary": "Выбрать библиотеку",
"filtersWithCount": "Фильтры (${count})",
"noRecommendations": "Рекомендации недоступны",
"noCollections": "В этой библиотеке нет коллекций",
"noFoldersFound": "Папки не найдены",
"folders": "папки",
"tabs": {
"recommended": "Рекомендуемые",
"browse": "Обзор",
"collections": "Коллекции",
"playlists": "Плейлисты"
},
"groupings": {
"title": "Группировка",
"all": "Все",
"movies": "Фильмы",
"shows": "Сериалы",
"seasons": "Сезоны",
"episodes": "Эпизоды",
"folders": "Папки"
}
},
"about": {
"title": "О приложении",
"openSourceLicenses": "Лицензии открытого ПО",
"versionLabel": "Версия ${version}",
"appDescription": "Красивый клиент Plex на Flutter",
"viewLicensesDescription": "Просмотр лицензий сторонних библиотек"
},
"serverSelection": {
"allServerConnectionsFailed": "Не удалось подключиться ни к одному серверу. Проверьте сеть и попробуйте снова.",
"noServersFoundForAccount": "Серверы не найдены для ${username} (${email})",
"failedToLoadServers": "Не удалось загрузить серверы: ${error}"
},
"hubDetail": {
"title": "Название",
"releaseYear": "Год выпуска",
"dateAdded": "Дата добавления",
"rating": "Рейтинг",
"noItemsFound": "Элементы не найдены"
},
"logs": {
"clearLogs": "Очистить логи",
"copyLogs": "Скопировать логи",
"uploadLogs": "Загрузить логи"
},
"licenses": {
"relatedPackages": "Связанные пакеты",
"license": "Лицензия",
"licenseNumber": "Лицензия ${number}",
"licensesCount": "${count} лицензий"
},
"navigation": {
"libraries": "Библиотеки",
"downloads": "Загрузки",
"liveTv": "ТВ в прямом эфире"
},
"liveTv": {
"title": "ТВ в прямом эфире",
"guide": "Программа",
"noChannels": "Нет доступных каналов",
"noDvr": "DVR не настроен ни на одном сервере",
"noPrograms": "Нет данных о программах",
"live": "ЭФИР",
"reloadGuide": "Перезагрузить программу",
"now": "Сейчас",
"today": "Сегодня",
"midnight": "Полночь",
"overnight": "Ночь",
"morning": "Утро",
"daytime": "День",
"evening": "Вечер",
"lateNight": "Поздний вечер",
"whatsOn": "Что идёт",
"watchChannel": "Смотреть канал",
"favorites": "Избранное",
"reorderFavorites": "Изменить порядок избранного",
"joinSession": "Присоединиться к текущему сеансу",
"watchFromStart": "Смотреть сначала (${minutes} мин. назад)",
"watchLive": "Смотреть в прямом эфире",
"goToLive": "К прямому эфиру"
},
"collections": {
"title": "Коллекции",
"collection": "Коллекция",
"empty": "Коллекция пуста",
"unknownLibrarySection": "Невозможно удалить: неизвестный раздел библиотеки",
"deleteCollection": "Удалить коллекцию",
"deleteConfirm": "Вы уверены, что хотите удалить \"${title}\"? Это действие нельзя отменить.",
"deleted": "Коллекция удалена",
"deleteFailed": "Не удалось удалить коллекцию",
"deleteFailedWithError": "Не удалось удалить коллекцию: ${error}",
"failedToLoadItems": "Не удалось загрузить элементы коллекции: ${error}",
"selectCollection": "Выбрать коллекцию",
"collectionName": "Название коллекции",
"enterCollectionName": "Введите название коллекции",
"addedToCollection": "Добавлено в коллекцию",
"errorAddingToCollection": "Не удалось добавить в коллекцию",
"created": "Коллекция создана",
"removeFromCollection": "Удалить из коллекции",
"removeFromCollectionConfirm": "Удалить \"${title}\" из этой коллекции?",
"removedFromCollection": "Удалено из коллекции",
"removeFromCollectionFailed": "Не удалось удалить из коллекции",
"removeFromCollectionError": "Ошибка удаления из коллекции: ${error}",
"searchCollections": "Поиск коллекций..."
},
"playlists": {
"title": "Плейлисты",
"playlist": "Плейлист",
"noPlaylists": "Плейлисты не найдены",
"create": "Создать плейлист",
"playlistName": "Название плейлиста",
"enterPlaylistName": "Введите название плейлиста",
"delete": "Удалить плейлист",
"removeItem": "Удалить из плейлиста",
"smartPlaylist": "Умный плейлист",
"itemCount": "${count} элементов",
"oneItem": "1 элемент",
"emptyPlaylist": "Этот плейлист пуст",
"deleteConfirm": "Удалить плейлист?",
"deleteMessage": "Вы уверены, что хотите удалить \"${name}\"?",
"created": "Плейлист создан",
"deleted": "Плейлист удалён",
"itemAdded": "Добавлено в плейлист",
"itemRemoved": "Удалено из плейлиста",
"selectPlaylist": "Выбрать плейлист",
"errorCreating": "Не удалось создать плейлист",
"errorDeleting": "Не удалось удалить плейлист",
"errorLoading": "Не удалось загрузить плейлисты",
"errorAdding": "Не удалось добавить в плейлист",
"errorReordering": "Не удалось переупорядочить элемент плейлиста",
"errorRemoving": "Не удалось удалить из плейлиста"
},
"watchTogether": {
"title": "Смотреть вместе",
"description": "Смотрите контент синхронно с друзьями и семьёй",
"createSession": "Создать сессию",
"creating": "Создание...",
"joinSession": "Присоединиться к сессии",
"joining": "Подключение...",
"controlMode": "Режим управления",
"controlModeQuestion": "Кто может управлять воспроизведением?",
"hostOnly": "Только хост",
"anyone": "Все",
"hostingSession": "Хостинг сессии",
"inSession": "В сессии",
"sessionCode": "Код сессии",
"hostControlsPlayback": "Хост управляет воспроизведением",
"anyoneCanControl": "Любой может управлять воспроизведением",
"hostControls": "Управление хоста",
"anyoneControls": "Управление для всех",
"participants": "Участники",
"host": "Хост",
"hostBadge": "HOST",
"youAreHost": "Вы — хост",
"watchingWithOthers": "Смотрите с другими",
"endSession": "Завершить сессию",
"leaveSession": "Покинуть сессию",
"endSessionQuestion": "Завершить сессию?",
"leaveSessionQuestion": "Покинуть сессию?",
"endSessionConfirm": "Это завершит сессию для всех участников.",
"leaveSessionConfirm": "Вы будете удалены из сессии.",
"endSessionConfirmOverlay": "Это завершит сеанс просмотра для всех участников.",
"leaveSessionConfirmOverlay": "Вы будете отключены от сеанса просмотра.",
"end": "Завершить",
"leave": "Покинуть",
"syncing": "Синхронизация...",
"joinWatchSession": "Присоединиться к просмотру",
"enterCodeHint": "Введите 5-символьный код",
"pasteFromClipboard": "Вставить из буфера обмена",
"pleaseEnterCode": "Введите код сессии",
"codeMustBe5Chars": "Код сессии должен содержать 5 символов",
"joinInstructions": "Введите код сессии, предоставленный хостом, чтобы присоединиться к просмотру.",
"failedToCreate": "Не удалось создать сессию",
"failedToJoin": "Не удалось присоединиться к сессии",
"sessionCodeCopied": "Код сессии скопирован в буфер обмена",
"relayUnreachable": "Ретранслятор недоступен. Возможно, ваш провайдер блокирует подключение. Вы можете попробовать, но «Смотреть вместе» может не работать.",
"reconnectingToHost": "Переподключение к хосту...",
"currentPlayback": "Текущее воспроизведение",
"joinCurrentPlayback": "Присоединиться к текущему воспроизведению",
"joinCurrentPlaybackDescription": "Вернуться к тому, что сейчас смотрит хост",
"failedToOpenCurrentPlayback": "Не удалось открыть текущее воспроизведение",
"participantJoined": "${name} присоединился",
"participantLeft": "${name} вышел",
"participantPaused": "${name} поставил на паузу",
"participantResumed": "${name} возобновил",
"participantSeeked": "${name} перемотал",
"participantBuffering": "${name} буферизует",
"waitingForParticipants": "Ожидание загрузки у других...",
"recentRooms": "Недавние комнаты",
"renameRoom": "Переименовать комнату",
"removeRoom": "Удалить"
},
"downloads": {
"title": "Загрузки",
"manage": "Управление",
"tvShows": "Сериалы",
"movies": "Фильмы",
"noDownloads": "Загрузок пока нет",
"noDownloadsDescription": "Загруженный контент появится здесь для просмотра офлайн",
"downloadNow": "Загрузить",
"deleteDownload": "Удалить загрузку",
"retryDownload": "Повторить загрузку",
"downloadQueued": "Загрузка поставлена в очередь",
"serverErrorBitrate": "Ошибка сервера — файл может превышать лимит битрейта удалённого стриминга",
"episodesQueued": "${count} эпизодов поставлено в очередь загрузки",
"downloadDeleted": "Загрузка удалена",
"deleteConfirm": "Вы уверены, что хотите удалить \"${title}\"? Загруженный файл будет удалён с устройства.",
"deletingWithProgress": "Удаление ${title}... (${current} из ${total})",
"noDownloadsTree": "Нет загрузок",
"pauseAll": "Приостановить все",
"resumeAll": "Возобновить все",
"deleteAll": "Удалить все",
"selectVersion": "Выбрать версию",
"allEpisodes": "Все эпизоды",
"unwatchedOnly": "Только непросмотренные",
"nextNUnwatched": "Следующие ${count} непросмотренных",
"customAmount": "Указать количество...",
"howManyEpisodes": "Сколько эпизодов?",
"itemsQueued": "${count} элементов добавлено в очередь загрузки",
"keepSynced": "Синхронизировать",
"downloadOnce": "Скачать один раз",
"keepNUnwatched": "Хранить ${count} непросмотренных",
"editSyncRule": "Редактировать правило синхронизации",
"removeSyncRule": "Удалить правило синхронизации",
"removeSyncRuleConfirm": "Прекратить синхронизацию «${title}»? Скачанные эпизоды будут сохранены.",
"syncRuleCreated": "Правило синхронизации создано — хранится ${count} непросмотренных эпизодов",
"syncRuleUpdated": "Правило синхронизации обновлено",
"syncRuleRemoved": "Правило синхронизации удалено",
"syncedNewEpisodes": "Синхронизировано ${count} новых эпизодов для ${title}",
"activeSyncRules": "Правила синхронизации",
"noSyncRules": "Нет правил синхронизации",
"manageSyncRule": "Управление синхронизацией",
"editEpisodeCount": "Количество эпизодов",
"editSyncFilter": "Фильтр синхронизации",
"syncAllItems": "Синхронизация всех элементов",
"syncUnwatchedItems": "Синхронизация непросмотренных элементов",
"syncRuleListCreated": "Правило синхронизации создано"
},
"shaders": {
"title": "Шейдеры",
"noShaderDescription": "Без улучшения видео",
"nvscalerDescription": "Масштабирование NVIDIA для более чёткого видео",
"qualityFast": "Быстрый",
"qualityHQ": "Высокое качество",
"mode": "Режим",
"importShader": "Импортировать шейдер",
"customShaderDescription": "Пользовательский GLSL шейдер",
"shaderImported": "Шейдер импортирован",
"shaderImportFailed": "Не удалось импортировать шейдер",
"deleteShader": "Удалить шейдер",
"deleteShaderConfirm": "Удалить \"${name}\"?"
},
"companionRemote": {
"title": "Пульт управления",
"connectedTo": "Подключено к ${name}",
"session": {
"startingServer": "Запуск удалённого сервера...",
"failedToCreate": "Не удалось запустить удалённый сервер:",
"hostAddress": "Адрес хоста",
"connected": "Подключено",
"serverRunning": "Удалённый сервер активен",
"serverStopped": "Удалённый сервер остановлен",
"serverRunningDescription": "Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему",
"serverStoppedDescription": "Запустите сервер, чтобы разрешить подключение мобильных устройств",
"usePhoneToControl": "Используйте мобильное устройство для управления этим приложением",
"startServer": "Запустить сервер",
"stopServer": "Остановить сервер",
"minimize": "Свернуть"
},
"pairing": {
"discoveryDescription": "Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически",
"hostAddressHint": "192.168.1.100:48632",
"connecting": "Подключение...",
"searchingForDevices": "Поиск устройств...",
"noDevicesFound": "Устройства в вашей сети не найдены",
"noDevicesHint": "Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi",
"availableDevices": "Доступные устройства",
"manualConnection": "Ручное подключение",
"cryptoInitFailed": "Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.",
"validationHostRequired": "Введите адрес хоста",
"validationHostFormat": "Формат должен быть IP:порт (например, 192.168.1.100:48632)",
"connectionTimedOut": "Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.",
"sessionNotFound": "Устройство не найдено. Убедитесь, что Plezy запущен на хосте.",
"authFailed": "Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.",
"failedToConnect": "Не удалось подключиться: ${error}"
},
"remote": {
"disconnectConfirm": "Отключиться от удалённой сессии?",
"reconnecting": "Переподключение...",
"attemptOf": "Попытка ${current} из 5",
"retryNow": "Повторить сейчас",
"tabRemote": "Пульт",
"tabPlay": "Воспроизведение",
"tabMore": "Ещё",
"menu": "Меню",
"tabNavigation": "Навигация",
"tabDiscover": "Обзор",
"tabLibraries": "Библиотеки",
"tabSearch": "Поиск",
"tabDownloads": "Загрузки",
"tabSettings": "Настройки",
"previous": "Предыдущий",
"playPause": "Воспроизведение/Пауза",
"next": "Следующий",
"seekBack": "Назад",
"stop": "Стоп",
"seekForward": "Вперёд",
"volume": "Громкость",
"volumeDown": "Тише",
"volumeUp": "Громче",
"fullscreen": "Полноэкранный",
"subtitles": "Субтитры",
"audio": "Аудио",
"searchHint": "Поиск на десктопе..."
}
},
"videoSettings": {
"playbackSettings": "Настройки воспроизведения",
"playbackSpeed": "Скорость воспроизведения",
"sleepTimer": "Таймер сна",
"audioSync": "Синхронизация аудио",
"subtitleSync": "Синхронизация субтитров",
"hdr": "HDR",
"audioOutput": "Аудиовыход",
"performanceOverlay": "Оверлей производительности",
"audioPassthrough": "Сквозной вывод аудио",
"audioNormalization": "Нормализация аудио"
},
"externalPlayer": {
"title": "Внешний плеер",
"useExternalPlayer": "Использовать внешний плеер",
"useExternalPlayerDescription": "Открывать видео во внешнем приложении вместо встроенного плеера",
"selectPlayer": "Выбрать плеер",
"customPlayers": "Свои плееры",
"systemDefault": "Системный по умолчанию",
"addCustomPlayer": "Добавить свой плеер",
"playerName": "Название плеера",
"playerCommand": "Команда",
"playerPackage": "Имя пакета",
"playerUrlScheme": "URL-схема",
"off": "Выкл.",
"launchFailed": "Не удалось открыть внешний плеер",
"appNotInstalled": "${name} не установлен",
"playInExternalPlayer": "Воспроизвести во внешнем плеере"
},
"metadataEdit": {
"editMetadata": "Редактировать...",
"screenTitle": "Редактировать метаданные",
"basicInfo": "Основная информация",
"artwork": "Обложка",
"advancedSettings": "Дополнительные настройки",
"title": "Название",
"sortTitle": "Название для сортировки",
"originalTitle": "Оригинальное название",
"releaseDate": "Дата выпуска",
"contentRating": "Возрастной рейтинг",
"studio": "Студия",
"tagline": "Слоган",
"summary": "Описание",
"poster": "Постер",
"background": "Фон",
"logo": "Логотип",
"squareArt": "Квадратное изображение",
"selectPoster": "Выбрать постер",
"selectBackground": "Выбрать фон",
"selectLogo": "Выбрать логотип",
"selectSquareArt": "Выбрать квадратное изображение",
"fromUrl": "По URL",
"uploadFile": "Загрузить файл",
"enterImageUrl": "Введите URL изображения",
"imageUrl": "URL изображения",
"metadataUpdated": "Метаданные обновлены",
"metadataUpdateFailed": "Не удалось обновить метаданные",
"artworkUpdated": "Обложка обновлена",
"artworkUpdateFailed": "Не удалось обновить обложку",
"noArtworkAvailable": "Обложки недоступны",
"notSet": "Не задано",
"libraryDefault": "По умолчанию библиотеки",
"accountDefault": "По умолчанию аккаунта",
"seriesDefault": "По умолчанию сериала",
"episodeSorting": "Сортировка эпизодов",
"oldestFirst": "Сначала старые",
"newestFirst": "Сначала новые",
"keep": "Сохранять",
"allEpisodes": "Все эпизоды",
"latestEpisodes": "${count} последних эпизодов",
"latestEpisode": "Последний эпизод",
"episodesAddedPastDays": "Эпизоды, добавленные за последние ${count} дней",
"deleteAfterPlaying": "Удалять эпизоды после просмотра",
"never": "Никогда",
"afterADay": "Через день",
"afterAWeek": "Через неделю",
"afterAMonth": "Через месяц",
"onNextRefresh": "При следующем обновлении",
"seasons": "Сезоны",
"show": "Показать",
"hide": "Скрыть",
"episodeOrdering": "Порядок эпизодов",
"tmdbAiring": "The Movie Database (Эфирный)",
"tvdbAiring": "TheTVDB (Эфирный)",
"tvdbAbsolute": "TheTVDB (Абсолютный)",
"metadataLanguage": "Язык метаданных",
"useOriginalTitle": "Использовать оригинальное название",
"preferredAudioLanguage": "Предпочитаемый язык аудио",
"preferredSubtitleLanguage": "Предпочитаемый язык субтитров",
"subtitleMode": "Автовыбор субтитров",
"manuallySelected": "Выбор вручную",
"shownWithForeignAudio": "Показывать при иноязычном аудио",
"alwaysEnabled": "Всегда включены",
"tags": "Теги",
"addTag": "Добавить тег",
"genre": "Жанр",
"director": "Режиссёр",
"writer": "Сценарист",
"producer": "Продюсер",
"country": "Страна",
"collection": "Коллекция",
"label": "Метка",
"style": "Стиль",
"mood": "Настроение"
},
"serverTasks": {
"title": "Задачи сервера",
"failedToLoad": "Не удалось загрузить задачи",
"noTasks": "Нет выполняемых задач"
},
"trakt": {
"title": "Trakt",
"connected": "Подключено",
"connectedAs": "Подключено как @${username}",
"disconnectConfirm": "Отключить аккаунт Trakt?",
"disconnectConfirmBody": "Plezy перестанет отправлять события воспроизведения в Trakt. Вы можете подключиться снова в любое время.",
"scrobble": "Скробблинг в реальном времени",
"scrobbleDescription": "Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.",
"watchedSync": "Синхронизация статуса просмотра",
"watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.",
"deviceCodeTitle": "Активация Plezy в Trakt",
"deviceCodeBody": "Откройте ${url} и введите этот код:",
"openTraktActivate": "Открыть Trakt для активации",
"waitingForAuthorization": "Ожидание авторизации…",
"codeCopied": "Код скопирован",
"connectFailed": "Не удалось подключиться к Trakt. Попробуйте ещё раз."
}
}
+93 -3
View File
@@ -3,10 +3,10 @@
/// Source: lib/i18n
/// To regenerate, run: `dart run slang`
///
/// Locales: 9
/// Strings: 7173 (797 per locale)
/// Locales: 15
/// Strings: 13260 (884 per locale)
///
/// Built on 2026-02-24 at 21:50 UTC
/// Built on 2026-04-19 at 14:14 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
@@ -18,12 +18,18 @@ import 'package:slang/generated.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
import 'strings_da.g.dart' deferred as l_da;
import 'strings_de.g.dart' deferred as l_de;
import 'strings_es.g.dart' deferred as l_es;
import 'strings_fr.g.dart' deferred as l_fr;
import 'strings_it.g.dart' deferred as l_it;
import 'strings_ja.g.dart' deferred as l_ja;
import 'strings_ko.g.dart' deferred as l_ko;
import 'strings_nb.g.dart' deferred as l_nb;
import 'strings_nl.g.dart' deferred as l_nl;
import 'strings_pl.g.dart' deferred as l_pl;
import 'strings_pt.g.dart' deferred as l_pt;
import 'strings_ru.g.dart' deferred as l_ru;
import 'strings_sv.g.dart' deferred as l_sv;
import 'strings_zh.g.dart' deferred as l_zh;
part 'strings_en.g.dart';
@@ -36,12 +42,18 @@ part 'strings_en.g.dart';
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
en(languageCode: 'en'),
da(languageCode: 'da'),
de(languageCode: 'de'),
es(languageCode: 'es'),
fr(languageCode: 'fr'),
it(languageCode: 'it'),
ja(languageCode: 'ja'),
ko(languageCode: 'ko'),
nb(languageCode: 'nb'),
nl(languageCode: 'nl'),
pl(languageCode: 'pl'),
pt(languageCode: 'pt'),
ru(languageCode: 'ru'),
sv(languageCode: 'sv'),
zh(languageCode: 'zh');
@@ -68,6 +80,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.da:
await l_da.loadLibrary();
return l_da.TranslationsDa(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.de:
await l_de.loadLibrary();
return l_de.TranslationsDe(
@@ -96,6 +115,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ja:
await l_ja.loadLibrary();
return l_ja.TranslationsJa(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ko:
await l_ko.loadLibrary();
return l_ko.TranslationsKo(
@@ -103,6 +129,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.nb:
await l_nb.loadLibrary();
return l_nb.TranslationsNb(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.nl:
await l_nl.loadLibrary();
return l_nl.TranslationsNl(
@@ -110,6 +143,27 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.pl:
await l_pl.loadLibrary();
return l_pl.TranslationsPl(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.pt:
await l_pt.loadLibrary();
return l_pt.TranslationsPt(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ru:
await l_ru.loadLibrary();
return l_ru.TranslationsRu(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.sv:
await l_sv.loadLibrary();
return l_sv.TranslationsSv(
@@ -140,6 +194,12 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.da:
return l_da.TranslationsDa(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.de:
return l_de.TranslationsDe(
overrides: overrides,
@@ -164,18 +224,48 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ja:
return l_ja.TranslationsJa(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ko:
return l_ko.TranslationsKo(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.nb:
return l_nb.TranslationsNb(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.nl:
return l_nl.TranslationsNl(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.pl:
return l_pl.TranslationsPl(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.pt:
return l_pt.TranslationsPt(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.ru:
return l_ru.TranslationsRu(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.sv:
return l_sv.TranslationsSv(
overrides: overrides,
File diff suppressed because it is too large Load Diff
+513 -339
View File
File diff suppressed because it is too large Load Diff
+718 -385
View File
File diff suppressed because it is too large Load Diff
+412 -238
View File
File diff suppressed because it is too large Load Diff
+428 -254
View File
File diff suppressed because it is too large Load Diff
+511 -337
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+406 -232
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+515 -341
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+511 -337
View File
File diff suppressed because it is too large Load Diff
+511 -337
View File
File diff suppressed because it is too large Load Diff
+248 -165
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "Logga in med Plex",
"showQRCode": "Visa QR-kod",
"authenticate": "Autentisera",
"debugEnterToken": "Debug: Ange Plex-token",
"plexTokenLabel": "Plex-autentiseringstoken",
"plexTokenHint": "Ange din Plex.tv-token",
"authenticationTimeout": "Autentisering tog för lång tid. Försök igen.",
"scanQRToSignIn": "Skanna QR-koden för att logga in",
"waitingForAuth": "Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.",
@@ -32,8 +29,7 @@
"delete": "Ta bort",
"shuffle": "Blanda",
"addTo": "Lägg till i...",
"remove": "Ta bort",
"paste": "Klistra in",
"createNew": "Skapa ny",
"connect": "Anslut",
"disconnect": "Koppla från",
"play": "Spela",
@@ -43,10 +39,9 @@
"search": "Sök",
"home": "Hem",
"back": "Tillbaka",
"settings": "Inställningar",
"settings": "Mer",
"mute": "Ljud av",
"ok": "OK",
"loading": "Laddar...",
"reconnect": "Återanslut",
"exitConfirmTitle": "Avsluta appen?",
"exitConfirmMessage": "Är du säker på att du vill avsluta?",
@@ -57,13 +52,14 @@
"refreshingServers": "Uppdaterar servrar...",
"loadingServers": "Laddar servrar...",
"connectingToServers": "Ansluter till servrar...",
"startingOfflineMode": "Startar offlineläge..."
"startingOfflineMode": "Startar offlineläge...",
"loading": "Laddar..."
},
"screens": {
"licenses": "Licenser",
"switchProfile": "Byt profil",
"subtitleStyling": "Undertext-styling",
"mpvConfig": "MPV-konfiguration",
"mpvConfig": "mpv.conf",
"logs": "Loggar"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "Inställningar",
"supportDeveloper": "Stöd Plezy",
"supportDeveloperDescription": "Donera via Liberapay för att finansiera utvecklingen",
"language": "Språk",
"theme": "Tema",
"appearance": "Utseende",
"videoPlayback": "Videouppspelning",
"videoPlaybackDescription": "Konfigurera uppspelningsbeteende",
"advanced": "Avancerat",
"episodePosterMode": "Avsnittsaffisch-stil",
"seriesPoster": "Serieaffisch",
"seriesPosterDescription": "Visa seriens affisch för alla avsnitt",
"seasonPoster": "Säsongsaffisch",
"seasonPosterDescription": "Visa säsongens affisch för avsnitt",
"episodeThumbnail": "Avsnittsminiatyr",
"episodeThumbnailDescription": "Visa 16:9 skärmbild från avsnittet",
"episodeThumbnail": "Miniatyr",
"showHeroSectionDescription": "Visa utvalda innehållskarusell på startsidan",
"secondsLabel": "Sekunder",
"minutesLabel": "Minuter",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "Ange tid (${min}-${max})",
"systemTheme": "System",
"systemThemeDescription": "Följ systeminställningar",
"lightTheme": "Ljust",
"darkTheme": "Mörkt",
"oledTheme": "OLED",
"oledThemeDescription": "Rent svart för OLED-skärmar",
"libraryDensity": "Biblioteksdensitet",
"compact": "Kompakt",
"compactDescription": "Mindre kort, fler objekt synliga",
"normal": "Normal",
"normalDescription": "Standardstorlek",
"comfortable": "Bekväm",
"comfortableDescription": "Större kort, färre objekt synliga",
"viewMode": "Visningsläge",
"gridView": "Rutnät",
"gridViewDescription": "Visa objekt i rutnätslayout",
"listView": "Lista",
"listViewDescription": "Visa objekt i listlayout",
"showHeroSection": "Visa hjältesektion",
"useGlobalHubs": "Använd Plex hem-layout",
"useGlobalHubsDescription": "Visar startsidans hubbar som den officiella Plex-klienten. När av visas rekommendationer per bibliotek istället.",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "Gör miniatyrer suddiga och dölj beskrivningar för avsnitt du inte har sett ännu",
"playerBackend": "Spelarmotor",
"exoPlayer": "ExoPlayer (Rekommenderad)",
"exoPlayerDescription": "Android-nativ spelare med bättre hårdvarustöd",
"mpv": "MPV",
"mpvDescription": "Avancerad spelare med fler funktioner och ASS-undertextstöd",
"mpv": "mpv",
"hardwareDecoding": "Hårdvaruavkodning",
"hardwareDecodingDescription": "Använd hårdvaruacceleration när tillgängligt",
"bufferSize": "Bufferstorlek",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "Anpassa undertextutseende",
"smallSkipDuration": "Kort hoppvaraktighet",
"largeSkipDuration": "Lång hoppvaraktighet",
"rewindOnResume": "Spola tillbaka vid återupptagning",
"secondsUnit": "${seconds} sekunder",
"defaultSleepTimer": "Standard sovtimer",
"minutesUnit": "${minutes} minuter",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "Anpassa tangentbordsgenvägar",
"videoPlayerNavigation": "Navigering i videospelaren",
"videoPlayerNavigationDescription": "Använd piltangenter för att navigera videospelarens kontroller",
"watchTogetherRelay": "Titta Tillsammans-relay",
"watchTogetherRelayDescription": "Ange en anpassad relay-server för Titta Tillsammans. Alla deltagare måste använda samma server.",
"watchTogetherRelayHint": "https://min-relay.exempel.se",
"crashReporting": "Kraschrapportering",
"crashReportingDescription": "Skicka kraschrapporter för att förbättra appen",
"debugLogging": "Felsökningsloggning",
"debugLoggingDescription": "Aktivera detaljerad loggning för felsökning",
"viewLogs": "Visa loggar",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "Hoppa automatiskt över credits och spela nästa avsnitt",
"autoSkipDelay": "Fördröjning Auto Hoppa Över",
"autoSkipDelayDescription": "Vänta ${seconds} sekunder innan automatisk överhoppning",
"introPattern": "Intromarkörsmönster",
"introPatternDescription": "Reguljärt uttryck för att matcha intromarkörer i kapiteltitlar",
"creditsPattern": "Eftertextmarkörsmönster",
"creditsPatternDescription": "Reguljärt uttryck för att matcha eftertextmarkörer i kapiteltitlar",
"invalidRegex": "Ogiltigt reguljärt uttryck",
"downloads": "Nedladdningar",
"downloadLocationDescription": "Välj var nedladdat innehåll ska lagras",
"downloadLocationDefault": "Standard (App-lagring)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "Kunde inte välja mapp",
"downloadOnWifiOnly": "Ladda ner endast på WiFi",
"downloadOnWifiOnlyDescription": "Förhindra nedladdningar vid användning av mobildata",
"autoRemoveWatchedDownloads": "Ta bort sedda nedladdningar automatiskt",
"autoRemoveWatchedDownloadsDescription": "Ta automatiskt bort nedladdade avsnitt och filmer när de markerats som sedda",
"cellularDownloadBlocked": "Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.",
"maxVolume": "Maximal volym",
"maxVolumeDescription": "Tillåt volym över 100% för tyst media",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord Rich Presence",
"discordRichPresenceDescription": "Visa vad du tittar på i Discord",
"trakt": "Trakt",
"traktDescription": "Synkronisera visningshistorik med Trakt",
"companionRemoteServer": "Companion Remote-server",
"companionRemoteServerDescription": "Tillåt mobila enheter i ditt nätverk att styra denna app",
"autoPip": "Automatisk bild-i-bild",
"autoPipDescription": "Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning",
"matchContentFrameRate": "Matcha innehållets bildfrekvens",
"matchContentFrameRateDescription": "Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "Matcha uppdateringsfrekvens",
"matchRefreshRateDescription": "Byt skärmens uppdateringsfrekvens för att matcha videoinnehåll i helskärm",
"matchDynamicRange": "Matcha dynamiskt omfång",
"matchDynamicRangeDescription": "Aktivera HDR automatiskt för HDR-innehåll och återgå till SDR när spelaren stängs",
"displaySwitchDelay": "Fördröjning vid skärmbyte",
"tunneledPlayback": "Tunneluppspelning",
"tunneledPlaybackDescription": "Använd hårdvaruaccelererad videotunnling. Inaktivera om du ser en svart skärm med ljud vid HDR-innehåll",
"requireProfileSelectionOnOpen": "Fråga efter profil vid appstart",
"requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas",
"confirmExitOnBack": "Bekräfta innan avslut",
"confirmExitOnBackDescription": "Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen"
"confirmExitOnBackDescription": "Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen",
"forceTvMode": "Tvinga TV-läge",
"forceTvModeDescription": "Använd TV-layouten oavsett automatisk identifiering. Användbart på Android TV-enheter som inte rapporterar leanback-funktionen. Startar om appen vid ändring.",
"autoHidePerformanceOverlay": "Dölj prestandaöverlagring automatiskt",
"autoHidePerformanceOverlayDescription": "Tona prestandaöverlagringen med uppspelningskontrollerna",
"showNavBarLabels": "Visa navigeringsfältets etiketter",
"showNavBarLabelsDescription": "Visa textetiketter under navigeringsfältets ikoner",
"liveTvDefaultFavorites": "Standard till favoritkanaler",
"liveTvDefaultFavoritesDescription": "Visa bara favoritkanaler när du öppnar Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Sök filmer, serier, musik...",
@@ -239,11 +263,6 @@
"skipMarker": "Hoppa över intro/eftertexter"
}
},
"pinEntry": {
"enterPin": "Ange PIN",
"showPin": "Visa PIN",
"hidePin": "Dölj PIN"
},
"fileInfo": {
"title": "Filinformation",
"video": "Video",
@@ -262,6 +281,8 @@
"colorPrimaries": "Färggrunder",
"chromaSubsampling": "Kroma-undersampling",
"channels": "Kanaler",
"subtitles": "Undertexter",
"overallBitrate": "Total bithastighet",
"path": "Sökväg",
"size": "Storlek",
"container": "Container",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "Detta inkluderar alla avsnitt och deras filer.",
"mediaDeletedSuccessfully": "Mediaobjekt borttaget",
"mediaFailedToDelete": "Kunde inte ta bort mediaobjekt",
"rate": "Betygsätt"
"rate": "Betygsätt",
"playFromBeginning": "Spela från början",
"playVersion": "Spela version..."
},
"accessibility": {
"mediaCardMovie": "${title}, film",
@@ -316,7 +339,6 @@
"unlockRotation": "Lås upp rotation",
"timerActive": "Timer aktiv",
"playbackWillPauseIn": "Uppspelningen pausas om ${duration}",
"sleepTimerCompleted": "Sovtimer slutförd - uppspelning pausad",
"stillWatching": "Tittar du fortfarande?",
"pausingIn": "Pausar om ${seconds}s",
"continueWatching": "Fortsätt",
@@ -333,34 +355,43 @@
"muteButton": "Tysta",
"unmuteButton": "Slå på ljud",
"settingsButton": "Videoinställningar",
"audioTrackButton": "Ljudspår",
"subtitlesButton": "Undertexter",
"tracksButton": "Ljud och undertexter",
"chaptersButton": "Kapitel",
"versionsButton": "Videoversioner",
"pipButton": "Bild-i-bild läge",
"aspectRatioButton": "Bildförhållande",
"ambientLighting": "Ambientbelysning",
"ambientLightingOn": "Aktivera ambientbelysning",
"ambientLightingOff": "Inaktivera ambientbelysning",
"fullscreenButton": "Aktivera helskärm",
"exitFullscreenButton": "Avsluta helskärm",
"alwaysOnTopButton": "Alltid överst",
"rotationLockButton": "Rotationslås",
"lockScreen": "Lås skärm",
"screenLockButton": "Skärmlås",
"longPressToUnlock": "Tryck länge för att låsa upp",
"timelineSlider": "Videotidslinje",
"volumeSlider": "Volymnivå",
"endsAt": "Slutar ${time}",
"pipActive": "Spelar i bild-i-bild",
"pipFailed": "Bild-i-bild kunde inte starta",
"pipErrors": {
"androidVersion": "Kräver Android 8.0 eller nyare",
"iosVersion": "Kräver iOS 15.0 eller nyare",
"permissionDisabled": "Bild-i-bild-behörighet är inaktiverad. Aktivera den i Inställningar > Appar > Plezy > Bild-i-bild",
"notSupported": "Denna enhet stöder inte bild-i-bild-läge",
"voSwitchFailed": "Kunde inte byta videoutgång för bild-i-bild",
"failed": "Bild-i-bild kunde inte starta",
"unknown": "Ett fel uppstod: ${error}"
},
"chapters": "Kapitel",
"noChaptersAvailable": "Inga kapitel tillgängliga",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "",
"noQueueItems": "Inga objekt i kön",
"searchSubtitles": "Sök undertexter",
"language": "Språk",
"noSubtitlesFound": "Inga undertexter hittades",
"subtitleDownloaded": "Undertext nedladdad",
"subtitleDownloadFailed": "Kunde inte ladda ner undertext",
"searchLanguages": "Sök språk..."
},
"userStatus": {
"admin": "Admin",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "Markerad som osedd",
"markedAsWatchedOffline": "Markerad som sedd (synkroniseras när online)",
"markedAsUnwatchedOffline": "Markerad som osedd (synkroniseras när online)",
"autoRemovedWatchedDownload": "Automatiskt borttagen: ${title}",
"removedFromContinueWatching": "Borttagen från Fortsätt titta",
"errorLoading": "Fel: ${error}",
"fileInfoNotAvailable": "Filinformation inte tillgänglig",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "Det gick inte att skapa uppspelningskö inga objekt",
"failedPlayback": "Kunde inte ${action}: ${error}",
"switchingToCompatiblePlayer": "Byter till kompatibel spelare...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "Uppspelningen misslyckades",
"serverLimitBody": "Servern returnerade ett fel (HTTP 500). Det betyder oftast att serverägaren har satt en gräns för bandbredd eller transkodning som avvisar din session. Det finns inget du kan göra från klienten — serverägaren behöver justera sina inställningar.",
"logsUploaded": "Loggar uppladdade",
"logsUploadFailed": "Uppladdning av loggar misslyckades",
"logId": "Logg-ID"
},
"subtitlingStyling": {
"stylingOptions": "Stilalternativ",
"text": "Text",
"border": "Kantlinje",
"background": "Bakgrund",
"fontSize": "Teckenstorlek",
"textColor": "Textfärg",
"borderSize": "Kantstorlek",
"borderColor": "Kantfärg",
"backgroundOpacity": "Bakgrundsopacitet",
"backgroundColor": "Bakgrundsfärg",
"position": "Position"
"position": "Position",
"assOverride": "ASS-åsidosättning",
"bold": "Fet",
"italic": "Kursiv"
},
"mpvConfig": {
"title": "MPV-konfiguration",
"title": "mpv-konfiguration",
"description": "Avancerade videospelares inställningar",
"properties": "Egenskaper",
"presets": "Förval",
"noProperties": "Inga egenskaper konfigurerade",
"noPresets": "Inga sparade förval",
"addProperty": "Lägg till egenskap",
"editProperty": "Redigera egenskap",
"deleteProperty": "Ta bort egenskap",
"propertyKey": "Egenskapsnyckel",
"propertyKeyHint": "t.ex. hwdec, demuxer-max-bytes",
"propertyValue": "Egenskapsvärde",
"propertyValueHint": "t.ex. auto, 256000000",
"saveAsPreset": "Spara som förval...",
"presetName": "Förvalnamn",
"presetNameHint": "Ange ett namn för detta förval",
@@ -436,8 +466,7 @@
"presetLoaded": "Förval laddat",
"presetDeleted": "Förval borttaget",
"confirmDeletePreset": "Är du säker på att du vill ta bort detta förval?",
"confirmDeleteProperty": "Är du säker på att du vill ta bort denna egenskap?",
"entriesCount": "${count} poster"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "Bekräfta åtgärd"
@@ -452,11 +481,8 @@
"overview": "Översikt",
"cast": "Rollbesättning",
"extras": "Trailers och Extra",
"seasons": "Säsonger",
"studio": "Studio",
"rating": "Åldersgräns",
"episodeCount": "${count} avsnitt",
"watchedProgress": "${watched}/${total} sedda",
"movie": "Film",
"tvShow": "TV-serie",
"minutesLeft": "${minutes} min kvar"
@@ -518,6 +544,7 @@
"playlists": "Spellistor"
},
"groupings": {
"title": "Gruppering",
"all": "Alla",
"movies": "Filmer",
"shows": "Serier",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "Rensa loggar",
"copyLogs": "Kopiera loggar",
"uploadLogs": "Upload Logs",
"error": "Fel:",
"stackTrace": "Stack trace:"
"uploadLogs": "Ladda upp loggar"
},
"licenses": {
"relatedPackages": "Relaterade paket",
@@ -560,25 +585,17 @@
},
"navigation": {
"libraries": "Bibliotek",
"downloads": "Nedladdningar",
"downloads": "Nerladdat",
"liveTv": "Live-TV"
},
"liveTv": {
"title": "Live-TV",
"channels": "Kanaler",
"guide": "Programguide",
"noChannels": "Inga kanaler tillgängliga",
"noDvr": "Ingen DVR konfigurerad på någon server",
"tuneFailed": "Kunde inte ställa in kanalen",
"loading": "Laddar kanaler...",
"nowPlaying": "Spelas nu",
"noPrograms": "Ingen programdata tillgänglig",
"channelNumber": "Kanal ${number}",
"live": "LIVE",
"hd": "HD",
"premiere": "NY",
"reloadGuide": "Ladda om programguide",
"allChannels": "Alla kanaler",
"now": "Nu",
"today": "Idag",
"midnight": "Midnatt",
@@ -588,30 +605,41 @@
"evening": "Kväll",
"lateNight": "Sen kväll",
"whatsOn": "På TV nu",
"watchChannel": "Titta på kanal"
"watchChannel": "Titta på kanal",
"favorites": "Favoriter",
"reorderFavorites": "Ordna om favoriter",
"joinSession": "Gå med i pågående session",
"watchFromStart": "Titta från början (${minutes} min sedan)",
"watchLive": "Titta live",
"goToLive": "Gå till live"
},
"downloads": {
"title": "Nedladdningar",
"manage": "Hantera",
"tvShows": "TV-serier",
"movies": "Filmer",
"noDownloads": "Inga nedladdningar ännu",
"noDownloadsDescription": "Nedladdat innehåll visas här för offline-visning",
"downloadNow": "Ladda ner",
"deleteDownload": "Ta bort nedladdning",
"retryDownload": "Försök igen",
"downloadQueued": "Nedladdning köad",
"episodesQueued": "${count} avsnitt köade för nedladdning",
"downloadDeleted": "Nedladdning borttagen",
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Den nedladdade filen kommer att tas bort från din enhet.",
"deletingWithProgress": "Tar bort ${title}... (${current} av ${total})",
"noDownloadsTree": "Inga nedladdningar",
"pauseAll": "Pausa alla",
"resumeAll": "Återuppta alla",
"deleteAll": "Ta bort alla"
"collections": {
"title": "Samlingar",
"collection": "Samling",
"empty": "Samlingen är tom",
"unknownLibrarySection": "Kan inte ta bort: okänd bibliotekssektion",
"deleteCollection": "Ta bort samling",
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Detta går inte att ångra.",
"deleted": "Samling borttagen",
"deleteFailed": "Det gick inte att ta bort samlingen",
"deleteFailedWithError": "Det gick inte att ta bort samlingen: ${error}",
"failedToLoadItems": "Det gick inte att läsa in samlingsobjekt: ${error}",
"selectCollection": "Välj samling",
"collectionName": "Samlingsnamn",
"enterCollectionName": "Ange samlingsnamn",
"addedToCollection": "Tillagd i samling",
"errorAddingToCollection": "Fel vid tillägg i samling",
"created": "Samling skapad",
"removeFromCollection": "Ta bort från samling",
"removeFromCollectionConfirm": "Ta bort \"${title}\" från denna samling?",
"removedFromCollection": "Borttagen från samling",
"removeFromCollectionFailed": "Misslyckades med att ta bort från samling",
"removeFromCollectionError": "Fel vid borttagning från samling: ${error}",
"searchCollections": "Sök samlingar..."
},
"playlists": {
"title": "Spellistor",
"playlist": "Spellista",
"noPlaylists": "Inga spellistor hittades",
"create": "Skapa spellista",
"playlistName": "Spellistans namn",
@@ -629,38 +657,12 @@
"itemAdded": "Tillagd i spellista",
"itemRemoved": "Borttagen från spellista",
"selectPlaylist": "Välj spellista",
"createNewPlaylist": "Skapa ny spellista",
"errorCreating": "Det gick inte att skapa spellista",
"errorDeleting": "Det gick inte att ta bort spellista",
"errorLoading": "Det gick inte att ladda spellistor",
"errorAdding": "Det gick inte att lägga till i spellista",
"errorReordering": "Det gick inte att omordna spellisteobjekt",
"errorRemoving": "Det gick inte att ta bort från spellista",
"playlist": "Spellista"
},
"collections": {
"title": "Samlingar",
"collection": "Samling",
"empty": "Samlingen är tom",
"unknownLibrarySection": "Kan inte ta bort: okänd bibliotekssektion",
"deleteCollection": "Ta bort samling",
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Detta går inte att ångra.",
"deleted": "Samling borttagen",
"deleteFailed": "Det gick inte att ta bort samlingen",
"deleteFailedWithError": "Det gick inte att ta bort samlingen: ${error}",
"failedToLoadItems": "Det gick inte att läsa in samlingsobjekt: ${error}",
"selectCollection": "Välj samling",
"createNewCollection": "Skapa ny samling",
"collectionName": "Samlingsnamn",
"enterCollectionName": "Ange samlingsnamn",
"addedToCollection": "Tillagd i samling",
"errorAddingToCollection": "Fel vid tillägg i samling",
"created": "Samling skapad",
"removeFromCollection": "Ta bort från samling",
"removeFromCollectionConfirm": "Ta bort \"${title}\" från denna samling?",
"removedFromCollection": "Borttagen från samling",
"removeFromCollectionFailed": "Misslyckades med att ta bort från samling",
"removeFromCollectionError": "Fel vid borttagning från samling: ${error}"
"errorRemoving": "Det gick inte att ta bort från spellista"
},
"watchTogether": {
"title": "Titta Tillsammans",
@@ -697,18 +699,76 @@
"leave": "Lämna",
"syncing": "Synkroniserar...",
"joinWatchSession": "Gå med i Tittarsession",
"enterCodeHint": "Ange 8-teckens kod",
"enterCodeHint": "Ange 5-teckens kod",
"pasteFromClipboard": "Klistra in från urklipp",
"pleaseEnterCode": "Vänligen ange en sessionskod",
"codeMustBe8Chars": "Sessionskod måste vara 8 tecken",
"codeMustBe5Chars": "Sessionskod måste vara 5 tecken",
"joinInstructions": "Ange sessionskoden som delats av värden för att gå med i deras tittarsession.",
"failedToCreate": "Det gick inte att skapa session",
"failedToJoin": "Det gick inte att gå med i session",
"sessionCodeCopied": "Sessionskod kopierad till urklipp",
"relayUnreachable": "Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.",
"reconnectingToHost": "Återansluter till värd...",
"currentPlayback": "Aktuell uppspelning",
"joinCurrentPlayback": "Gå med i aktuell uppspelning",
"joinCurrentPlaybackDescription": "Hoppa tillbaka till det värden tittar på just nu",
"failedToOpenCurrentPlayback": "Kunde inte öppna aktuell uppspelning",
"participantJoined": "${name} gick med",
"participantLeft": "${name} lämnade"
"participantLeft": "${name} lämnade",
"participantPaused": "${name} pausade",
"participantResumed": "${name} återupptog",
"participantSeeked": "${name} spolade",
"participantBuffering": "${name} buffrar",
"waitingForParticipants": "Väntar på att andra laddar...",
"recentRooms": "Senaste rum",
"renameRoom": "Byt namn på rum",
"removeRoom": "Ta bort"
},
"downloads": {
"title": "Nedladdningar",
"manage": "Hantera",
"tvShows": "TV-serier",
"movies": "Filmer",
"noDownloads": "Inga nedladdningar ännu",
"noDownloadsDescription": "Nedladdat innehåll visas här för offline-visning",
"downloadNow": "Ladda ner",
"deleteDownload": "Ta bort nedladdning",
"retryDownload": "Försök igen",
"downloadQueued": "Nedladdning köad",
"serverErrorBitrate": "Serverfel — filen överskrider möjligen gränsen för fjärrströmning-bitrate",
"episodesQueued": "${count} avsnitt köade för nedladdning",
"downloadDeleted": "Nedladdning borttagen",
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Den nedladdade filen kommer att tas bort från din enhet.",
"deletingWithProgress": "Tar bort ${title}... (${current} av ${total})",
"noDownloadsTree": "Inga nedladdningar",
"pauseAll": "Pausa alla",
"resumeAll": "Återuppta alla",
"deleteAll": "Ta bort alla",
"selectVersion": "Välj version",
"allEpisodes": "Alla avsnitt",
"unwatchedOnly": "Endast osedda",
"nextNUnwatched": "Nästa ${count} osedda",
"customAmount": "Ange antal...",
"howManyEpisodes": "Hur många avsnitt?",
"itemsQueued": "${count} objekt köade för nedladdning",
"keepSynced": "Håll synkroniserad",
"downloadOnce": "Ladda ner en gång",
"keepNUnwatched": "Behåll ${count} osedda",
"editSyncRule": "Redigera synkregel",
"removeSyncRule": "Ta bort synkregel",
"removeSyncRuleConfirm": "Sluta synkronisera \"${title}\"? Nedladdade avsnitt behålls.",
"syncRuleCreated": "Synkregel skapad — behåller ${count} osedda avsnitt",
"syncRuleUpdated": "Synkregel uppdaterad",
"syncRuleRemoved": "Synkregel borttagen",
"syncedNewEpisodes": "Synkroniserade ${count} nya avsnitt för ${title}",
"activeSyncRules": "Synkregler",
"noSyncRules": "Inga synkregler",
"manageSyncRule": "Hantera synkronisering",
"editEpisodeCount": "Antal avsnitt",
"editSyncFilter": "Synkroniseringsfilter",
"syncAllItems": "Synkroniserar alla objekt",
"syncUnwatchedItems": "Synkroniserar osedda objekt",
"syncRuleListCreated": "Synkroniseringsregel skapad"
},
"shaders": {
"title": "Shaders",
@@ -716,58 +776,46 @@
"nvscalerDescription": "NVIDIA-bildskalning för skarpare video",
"qualityFast": "Snabb",
"qualityHQ": "Hög kvalitet",
"mode": "Läge"
"mode": "Läge",
"importShader": "Importera shader",
"customShaderDescription": "Anpassad GLSL-shader",
"shaderImported": "Shader importerad",
"shaderImportFailed": "Kunde inte importera shader",
"deleteShader": "Ta bort shader",
"deleteShaderConfirm": "Ta bort \"${name}\"?"
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "Anslut till enhet",
"hostRemoteSession": "Starta fjärrsession",
"controlThisDevice": "Styr den här enheten med din telefon",
"remoteControl": "Fjärrkontroll",
"controlDesktop": "Styr en datorenhet",
"title": "Fjärrkontroll",
"connectedTo": "Ansluten till ${name}",
"session": {
"creatingSession": "Skapar fjärrsession...",
"failedToCreate": "Kunde inte skapa fjärrsession:",
"noSession": "Ingen session tillgänglig",
"scanQrCode": "Skanna QR-kod",
"orEnterManually": "Eller ange manuellt",
"startingServer": "Startar fjärrserver...",
"failedToCreate": "Kunde inte starta fjärrserver:",
"hostAddress": "Värdadress",
"sessionId": "Sessions-ID",
"pin": "PIN",
"connected": "Ansluten",
"waitingForConnection": "Väntar på anslutning...",
"serverRunning": "Fjärrserver aktiv",
"serverStopped": "Fjärrserver stoppad",
"serverRunningDescription": "Mobila enheter i ditt nätverk kan upptäcka och ansluta till denna app",
"serverStoppedDescription": "Starta servern för att tillåta mobila enheter att ansluta",
"usePhoneToControl": "Använd din mobila enhet för att styra denna app",
"copiedToClipboard": "${label} kopierad till urklipp",
"copyToClipboard": "Kopiera till urklipp",
"newSession": "Ny session",
"startServer": "Starta server",
"stopServer": "Stoppa server",
"minimize": "Minimera"
},
"pairing": {
"scan": "Skanna",
"manual": "Manuell",
"pairWithDesktop": "Parkoppla med dator",
"enterSessionDetails": "Ange sessionsuppgifterna som visas på din datorenhet",
"discoveryDescription": "Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "Ange 8-teckens sessions-ID",
"pinHint": "Ange 6-siffrig PIN",
"connecting": "Ansluter...",
"tips": "Tips",
"tipDesktop": "Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny",
"tipScan": "Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator",
"tipWifi": "Se till att båda enheterna är på samma WiFi-nätverk",
"cameraPermissionRequired": "Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.",
"cameraError": "Kunde inte starta kameran: ${error}",
"scanInstruction": "Rikta kameran mot QR-koden som visas på din dator",
"invalidQrCode": "Ogiltigt QR-kodformat",
"validationHostRequired": "Ange en värdadress",
"searchingForDevices": "Söker efter enheter...",
"noDevicesFound": "Inga enheter hittades i ditt nätverk",
"noDevicesHint": "Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk",
"availableDevices": "Tillgängliga enheter",
"manualConnection": "Manuell anslutning",
"cryptoInitFailed": "Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.",
"validationHostRequired": "Ange värdadress",
"validationHostFormat": "Format måste vara IP:port (t.ex. 192.168.1.100:48632)",
"validationSessionIdRequired": "Ange ett sessions-ID",
"validationSessionIdLength": "Sessions-ID måste vara 8 tecken",
"validationPinRequired": "Ange en PIN",
"validationPinLength": "PIN måste vara 6 siffror",
"connectionTimedOut": "Anslutningen tog för lång tid. Kontrollera sessions-ID och PIN.",
"sessionNotFound": "Kunde inte hitta sessionen. Kontrollera dina uppgifter.",
"connectionTimedOut": "Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.",
"sessionNotFound": "Kunde inte hitta enheten. Se till att Plezy körs på värden.",
"authFailed": "Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.",
"failedToConnect": "Kunde inte ansluta: ${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "Återansluter...",
"attemptOf": "Försök ${current} av 5",
"retryNow": "Försök nu",
"connectionError": "Anslutningsfel",
"notConnected": "Inte ansluten",
"tabRemote": "Fjärrkontroll",
"tabPlay": "Spela",
"tabMore": "Mer",
@@ -819,13 +865,13 @@
"useExternalPlayer": "Använd extern spelare",
"useExternalPlayerDescription": "Öppna videor i en extern app istället för den inbyggda spelaren",
"selectPlayer": "Välj spelare",
"customPlayers": "Anpassade spelare",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Lägg till anpassad spelare",
"playerName": "Spelarnamn",
"playerCommand": "Kommando",
"playerPackage": "Paketnamn",
"playerUrlScheme": "URL-schema",
"customPlayer": "Anpassad spelare",
"off": "Av",
"launchFailed": "Kunde inte öppna extern spelare",
"appNotInstalled": "${name} är inte installerad",
@@ -847,8 +893,12 @@
"summary": "Sammanfattning",
"poster": "Poster",
"background": "Bakgrund",
"logo": "Logotyp",
"squareArt": "Kvadratisk bild",
"selectPoster": "Välj poster",
"selectBackground": "Välj bakgrund",
"selectLogo": "Välj logotyp",
"selectSquareArt": "Välj kvadratisk bild",
"fromUrl": "Från URL",
"uploadFile": "Ladda upp fil",
"enterImageUrl": "Ange bild-URL",
@@ -890,6 +940,39 @@
"subtitleMode": "Automatiskt val av undertexter",
"manuallySelected": "Manuellt vald",
"shownWithForeignAudio": "Visas vid främmande ljud",
"alwaysEnabled": "Alltid aktiverad"
"alwaysEnabled": "Alltid aktiverad",
"tags": "Taggar",
"addTag": "Lägg till tagg",
"genre": "Genre",
"director": "Regissör",
"writer": "Författare",
"producer": "Producent",
"country": "Land",
"collection": "Samling",
"label": "Etikett",
"style": "Stil",
"mood": "Stämning"
},
"serverTasks": {
"title": "Serveruppgifter",
"failedToLoad": "Kunde inte ladda uppgifter",
"noTasks": "Inga pågående uppgifter"
},
"trakt": {
"title": "Trakt",
"connected": "Ansluten",
"connectedAs": "Ansluten som @${username}",
"disconnectConfirm": "Koppla från Trakt-konto?",
"disconnectConfirmBody": "Plezy slutar skicka uppspelningshändelser till Trakt. Du kan ansluta igen när som helst.",
"scrobble": "Realtids-scrobbling",
"scrobbleDescription": "Skicka uppspelnings-, paus- och stopphändelser till Trakt under uppspelning.",
"watchedSync": "Synkronisera tittad-status",
"watchedSyncDescription": "När du markerar något som tittat i Plezy markeras det också på Trakt.",
"deviceCodeTitle": "Aktivera Plezy på Trakt",
"deviceCodeBody": "Besök ${url} och ange den här koden:",
"openTraktActivate": "Öppna Trakt för att aktivera",
"waitingForAuthorization": "Väntar på auktorisering…",
"codeCopied": "Kod kopierad",
"connectFailed": "Kunde inte ansluta till Trakt. Försök igen."
}
}
+247 -164
View File
@@ -6,9 +6,6 @@
"signInWithPlex": "使用 Plex 登录",
"showQRCode": "显示二维码",
"authenticate": "验证",
"debugEnterToken": "调试:输入 Plex Token",
"plexTokenLabel": "Plex 授权令牌 (Auth Token)",
"plexTokenHint": "输入你的 Plex.tv 令牌",
"authenticationTimeout": "验证超时。请重试。",
"scanQRToSignIn": "扫描二维码登录",
"waitingForAuth": "等待验证中...\n请在你的浏览器中完成登录。",
@@ -32,8 +29,7 @@
"delete": "删除",
"shuffle": "随机播放",
"addTo": "添加到...",
"remove": "删除",
"paste": "粘贴",
"createNew": "新建",
"connect": "连接",
"disconnect": "断开连接",
"play": "播放",
@@ -46,7 +42,6 @@
"settings": "设置",
"mute": "静音",
"ok": "确定",
"loading": "加载中...",
"reconnect": "重新连接",
"exitConfirmTitle": "退出应用?",
"exitConfirmMessage": "确定要退出吗?",
@@ -57,13 +52,14 @@
"refreshingServers": "正在刷新服务器...",
"loadingServers": "正在加载服务器...",
"connectingToServers": "正在连接服务器...",
"startingOfflineMode": "正在启动离线模式..."
"startingOfflineMode": "正在启动离线模式...",
"loading": "加载中..."
},
"screens": {
"licenses": "许可证",
"switchProfile": "切换用户",
"subtitleStyling": "字幕样式",
"mpvConfig": "MPV 配置",
"mpvConfig": "mpv.conf",
"logs": "日志"
},
"update": {
@@ -77,18 +73,18 @@
},
"settings": {
"title": "设置",
"supportDeveloper": "支持 Plezy",
"supportDeveloperDescription": "通过 Liberapay 捐赠支持开发",
"language": "语言",
"theme": "主题",
"appearance": "外观",
"videoPlayback": "视频播放",
"videoPlaybackDescription": "配置播放行为",
"advanced": "高级",
"episodePosterMode": "剧集海报样式",
"seriesPoster": "剧集海报",
"seriesPosterDescription": "为所有剧集显示剧集海报",
"seasonPoster": "季海报",
"seasonPosterDescription": "为剧集显示特定季的海报",
"episodeThumbnail": "剧集缩略图",
"episodeThumbnailDescription": "显示16:9剧集截图缩略图",
"episodeThumbnail": "缩略图",
"showHeroSectionDescription": "在主屏幕上显示精选内容轮播区",
"secondsLabel": "秒",
"minutesLabel": "分钟",
@@ -96,23 +92,15 @@
"minutesShort": "m",
"durationHint": "输入时长 (${min}-${max})",
"systemTheme": "系统",
"systemThemeDescription": "跟随系统设置",
"lightTheme": "浅色",
"darkTheme": "深色",
"oledTheme": "OLED",
"oledThemeDescription": "纯黑色适用于 OLED 屏幕",
"libraryDensity": "媒体库密度",
"compact": "紧凑",
"compactDescription": "卡片更小,显示更多项目",
"normal": "标准",
"normalDescription": "默认尺寸",
"comfortable": "舒适",
"comfortableDescription": "卡片更大,显示更少项目",
"viewMode": "视图模式",
"gridView": "网格视图",
"gridViewDescription": "以网格布局显示项目",
"listView": "列表视图",
"listViewDescription": "以列表布局显示项目",
"showHeroSection": "显示主要精选区",
"useGlobalHubs": "使用 Plex 主页布局",
"useGlobalHubsDescription": "显示与官方 Plex 客户端相同的主页推荐。关闭时将显示按媒体库分类的推荐。",
@@ -126,9 +114,7 @@
"hideSpoilersDescription": "模糊未观看剧集的缩略图并隐藏其描述",
"playerBackend": "播放器引擎",
"exoPlayer": "ExoPlayer(推荐)",
"exoPlayerDescription": "Android 原生播放器,硬件支持更好",
"mpv": "MPV",
"mpvDescription": "功能更多的高级播放器,支持 ASS 字幕",
"mpv": "mpv",
"hardwareDecoding": "硬件解码",
"hardwareDecodingDescription": "如果可用,使用硬件加速",
"bufferSize": "缓冲区大小",
@@ -139,6 +125,7 @@
"subtitleStylingDescription": "调整字幕外观",
"smallSkipDuration": "短跳过时长",
"largeSkipDuration": "长跳过时长",
"rewindOnResume": "恢复时回退",
"secondsUnit": "${seconds} 秒",
"defaultSleepTimer": "默认睡眠定时器",
"minutesUnit": "${minutes} 分钟",
@@ -151,6 +138,11 @@
"keyboardShortcutsDescription": "自定义键盘快捷键",
"videoPlayerNavigation": "视频播放器导航",
"videoPlayerNavigationDescription": "使用方向键导航视频播放器控件",
"watchTogetherRelay": "一起看中继服务器",
"watchTogetherRelayDescription": "设置一起看的自定义中继服务器。所有参与者必须使用相同的服务器。",
"watchTogetherRelayHint": "https://my-relay.example.com",
"crashReporting": "崩溃报告",
"crashReportingDescription": "发送崩溃报告以帮助改进应用",
"debugLogging": "调试日志",
"debugLoggingDescription": "启用详细日志记录以便故障排除",
"viewLogs": "查看日志",
@@ -178,6 +170,11 @@
"autoSkipCreditsDescription": "自动跳过片尾并播放下一集",
"autoSkipDelay": "自动跳过延迟",
"autoSkipDelayDescription": "自动跳过前等待 ${seconds} 秒",
"introPattern": "片头标记模式",
"introPatternDescription": "用于匹配章节标题中片头标记的正则表达式",
"creditsPattern": "片尾标记模式",
"creditsPatternDescription": "用于匹配章节标题中片尾标记的正则表达式",
"invalidRegex": "无效的正则表达式",
"downloads": "下载",
"downloadLocationDescription": "选择下载内容的存储位置",
"downloadLocationDefault": "默认(应用存储)",
@@ -191,22 +188,49 @@
"downloadLocationSelectError": "选择文件夹失败",
"downloadOnWifiOnly": "仅在 WiFi 时下载",
"downloadOnWifiOnlyDescription": "使用蜂窝数据时禁止下载",
"autoRemoveWatchedDownloads": "自动移除已观看的下载",
"autoRemoveWatchedDownloadsDescription": "当剧集和电影被标记为已观看时自动删除下载内容",
"cellularDownloadBlocked": "蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。",
"maxVolume": "最大音量",
"maxVolumeDescription": "允许音量超过 100% 以适应安静的媒体",
"maxVolumePercent": "${percent}%",
"discordRichPresence": "Discord 动态状态",
"discordRichPresenceDescription": "在 Discord 上显示您正在观看的内容",
"trakt": "Trakt",
"traktDescription": "将观看历史与 Trakt 同步",
"companionRemoteServer": "配套遥控服务器",
"companionRemoteServerDescription": "允许网络上的移动设备控制此应用",
"autoPip": "自动画中画",
"autoPipDescription": "在播放期间离开应用时自动进入画中画模式",
"matchContentFrameRate": "匹配内容帧率",
"matchContentFrameRateDescription": "调整显示刷新率以匹配视频内容,减少画面抖动并节省电量",
"tunneledPlayback": "Tunneled Playback",
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
"matchRefreshRate": "匹配刷新率",
"matchRefreshRateDescription": "全屏时切换显示刷新率以匹配视频内容",
"matchDynamicRange": "匹配动态范围",
"matchDynamicRangeDescription": "自动为HDR内容启用HDR,退出播放器时恢复为SDR",
"displaySwitchDelay": "显示切换延迟",
"tunneledPlayback": "通道化播放",
"tunneledPlaybackDescription": "使用硬件加速视频通道。如果在 HDR 内容上看到黑屏但有声音,请禁用此选项",
"requireProfileSelectionOnOpen": "打开应用时询问配置文件",
"requireProfileSelectionOnOpenDescription": "每次打开应用时显示配置文件选择",
"confirmExitOnBack": "退出前确认",
"confirmExitOnBackDescription": "按返回键退出应用时显示确认对话框"
"confirmExitOnBackDescription": "按返回键退出应用时显示确认对话框",
"forceTvMode": "强制 TV 模式",
"forceTvModeDescription": "无论自动检测结果如何都使用 TV 布局。适用于未报告 leanback 功能的 Android TV 设备。更改后将重启应用。",
"autoHidePerformanceOverlay": "自动隐藏性能叠加层",
"autoHidePerformanceOverlayDescription": "性能叠加层随播放控件一起淡入淡出",
"showNavBarLabels": "显示导航栏标签",
"showNavBarLabelsDescription": "在导航栏图标下方显示文字标签",
"liveTvDefaultFavorites": "默认显示收藏频道",
"liveTvDefaultFavoritesDescription": "打开直播电视时仅显示收藏频道",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "搜索电影、系列、音乐...",
@@ -239,11 +263,6 @@
"skipMarker": "跳过片头/片尾"
}
},
"pinEntry": {
"enterPin": "输入 PIN",
"showPin": "显示 PIN",
"hidePin": "隐藏 PIN"
},
"fileInfo": {
"title": "文件信息",
"video": "视频",
@@ -262,6 +281,8 @@
"colorPrimaries": "颜色原色",
"chromaSubsampling": "色度子采样",
"channels": "声道",
"subtitles": "字幕",
"overallBitrate": "总比特率",
"path": "路径",
"size": "大小",
"container": "容器",
@@ -282,7 +303,9 @@
"deleteMultipleWarning": "这包括所有剧集及其文件。",
"mediaDeletedSuccessfully": "媒体项已成功删除",
"mediaFailedToDelete": "删除媒体项失败",
"rate": "评分"
"rate": "评分",
"playFromBeginning": "从头播放",
"playVersion": "播放版本..."
},
"accessibility": {
"mediaCardMovie": "${title}, 电影",
@@ -316,7 +339,6 @@
"unlockRotation": "解锁旋转",
"timerActive": "定时器已激活",
"playbackWillPauseIn": "播放将在 ${duration} 后暂停",
"sleepTimerCompleted": "睡眠定时器已完成 - 播放已暂停",
"stillWatching": "还在看吗?",
"pausingIn": "${seconds}秒后暂停",
"continueWatching": "继续",
@@ -333,34 +355,43 @@
"muteButton": "静音",
"unmuteButton": "取消静音",
"settingsButton": "视频设置",
"audioTrackButton": "音",
"subtitlesButton": "字幕",
"tracksButton": "音频和字幕",
"chaptersButton": "章节",
"versionsButton": "视频版本",
"pipButton": "画中画模式",
"aspectRatioButton": "宽高比",
"ambientLighting": "氛围灯光",
"ambientLightingOn": "开启氛围灯光",
"ambientLightingOff": "关闭氛围灯光",
"fullscreenButton": "进入全屏",
"exitFullscreenButton": "退出全屏",
"alwaysOnTopButton": "置顶窗口",
"rotationLockButton": "旋转锁定",
"lockScreen": "锁定屏幕",
"screenLockButton": "屏幕锁定",
"longPressToUnlock": "长按解锁",
"timelineSlider": "视频时间轴",
"volumeSlider": "音量调节",
"endsAt": "${time} 结束",
"pipActive": "正在画中画模式中播放",
"pipFailed": "画中画启动失败",
"pipErrors": {
"androidVersion": "需要 Android 8.0 或更高版本",
"iosVersion": "需要 iOS 15.0 或更高版本",
"permissionDisabled": "画中画权限已禁用。请在设置 > 应用 > Plezy > 画中画中启用",
"notSupported": "此设备不支持画中画模式",
"voSwitchFailed": "无法切换画中画的视频输出",
"failed": "画中画启动失败",
"unknown": "发生错误:${error}"
},
"chapters": "章节",
"noChaptersAvailable": "没有可用的章节",
"queue": "Queue",
"noQueueItems": "No items in queue"
"queue": "播放队列",
"noQueueItems": "队列中没有项目",
"searchSubtitles": "搜索字幕",
"language": "语言",
"noSubtitlesFound": "未找到字幕",
"subtitleDownloaded": "字幕已下载",
"subtitleDownloadFailed": "字幕下载失败",
"searchLanguages": "搜索语言..."
},
"userStatus": {
"admin": "管理员",
@@ -373,6 +404,7 @@
"markedAsUnwatched": "已标记为未观看",
"markedAsWatchedOffline": "已标记为已观看 (将在联网时同步)",
"markedAsUnwatchedOffline": "已标记为未观看 (将在联网时同步)",
"autoRemovedWatchedDownload": "已自动移除: ${title}",
"removedFromContinueWatching": "已从继续观看中移除",
"errorLoading": "错误: ${error}",
"fileInfoNotAvailable": "文件信息不可用",
@@ -399,34 +431,32 @@
"failedToCreatePlayQueueNoItems": "创建播放队列失败 - 没有项目",
"failedPlayback": "无法${action}: ${error}",
"switchingToCompatiblePlayer": "正在切换到兼容的播放器...",
"logsUploaded": "Logs uploaded",
"logsUploadFailed": "Failed to upload logs",
"logId": "Log ID"
"serverLimitTitle": "播放失败",
"serverLimitBody": "服务器返回了错误 (HTTP 500)。这通常意味着服务器所有者设置了带宽或转码限制,正在拒绝你的会话。客户端无法解决此问题 — 需要服务器所有者调整其设置。",
"logsUploaded": "日志已上传",
"logsUploadFailed": "上传日志失败",
"logId": "日志 ID"
},
"subtitlingStyling": {
"stylingOptions": "样式选项",
"text": "文本",
"border": "边框",
"background": "背景",
"fontSize": "字号",
"textColor": "文本颜色",
"borderSize": "边框大小",
"borderColor": "边框颜色",
"backgroundOpacity": "背景不透明度",
"backgroundColor": "背景颜色",
"position": "Position"
"position": "位置",
"assOverride": "ASS 样式覆盖",
"bold": "粗体",
"italic": "斜体"
},
"mpvConfig": {
"title": "MPV 配置",
"title": "mpv 配置",
"description": "高级视频播放器设置",
"properties": "属性",
"presets": "预设",
"noProperties": "未配置任何属性",
"noPresets": "没有保存的预设",
"addProperty": "添加属性",
"editProperty": "编辑属性",
"deleteProperty": "删除属性",
"propertyKey": "属性键",
"propertyKeyHint": "例如 hwdec, demuxer-max-bytes",
"propertyValue": "属性值",
"propertyValueHint": "例如 auto, 256000000",
"saveAsPreset": "保存为预设...",
"presetName": "预设名称",
"presetNameHint": "输入此预设的名称",
@@ -436,8 +466,7 @@
"presetLoaded": "预设已加载",
"presetDeleted": "预设已删除",
"confirmDeletePreset": "确定要删除此预设吗?",
"confirmDeleteProperty": "确定要删除此属性吗?",
"entriesCount": "${count} 条目"
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
},
"dialog": {
"confirmAction": "确认操作"
@@ -452,11 +481,8 @@
"overview": "概述",
"cast": "演员表",
"extras": "预告片与花絮",
"seasons": "季数",
"studio": "制作公司",
"rating": "年龄分级",
"episodeCount": "${count} 集",
"watchedProgress": "已观看 ${watched}/${total} 集",
"movie": "电影",
"tvShow": "电视剧",
"minutesLeft": "剩余 ${minutes} 分钟"
@@ -518,6 +544,7 @@
"playlists": "播放列表"
},
"groupings": {
"title": "分组",
"all": "全部",
"movies": "电影",
"shows": "剧集",
@@ -548,9 +575,7 @@
"logs": {
"clearLogs": "清除日志",
"copyLogs": "复制日志",
"uploadLogs": "Upload Logs",
"error": "错误:",
"stackTrace": "堆栈跟踪 (Stack Trace):"
"uploadLogs": "上传日志"
},
"licenses": {
"relatedPackages": "相关软件包",
@@ -565,20 +590,12 @@
},
"liveTv": {
"title": "电视直播",
"channels": "频道",
"guide": "节目指南",
"noChannels": "没有可用的频道",
"noDvr": "没有服务器配置了DVR",
"tuneFailed": "无法调谐频道",
"loading": "正在加载频道...",
"nowPlaying": "正在播放",
"noPrograms": "没有可用的节目数据",
"channelNumber": "频道 ${number}",
"live": "直播",
"hd": "高清",
"premiere": "新",
"reloadGuide": "重新加载节目指南",
"allChannels": "所有频道",
"now": "现在",
"today": "今天",
"midnight": "午夜",
@@ -588,30 +605,41 @@
"evening": "晚上",
"lateNight": "深夜",
"whatsOn": "正在播出",
"watchChannel": "观看频道"
"watchChannel": "观看频道",
"favorites": "收藏",
"reorderFavorites": "重新排序收藏",
"joinSession": "加入正在进行的会话",
"watchFromStart": "从头观看(${minutes}分钟前开始)",
"watchLive": "观看直播",
"goToLive": "跳至直播"
},
"downloads": {
"title": "下载",
"manage": "管理",
"tvShows": "电视剧",
"movies": "电影",
"noDownloads": "暂无下载",
"noDownloadsDescription": "下载的内容将在此处显示以供离线观看",
"downloadNow": "下载",
"deleteDownload": "删除下载",
"retryDownload": "重试下载",
"downloadQueued": "下载已排队",
"episodesQueued": "${count} 集已加入下载队列",
"downloadDeleted": "下载已删除",
"deleteConfirm": "确定要删除 \"${title}\" 吗?下载的文件将从您的设备中删除。",
"deletingWithProgress": "正在删除 ${title}... (${current}/${total})",
"noDownloadsTree": "暂无下载",
"pauseAll": "全部暂停",
"resumeAll": "全部继续",
"deleteAll": "全部删除"
"collections": {
"title": "合集",
"collection": "合集",
"empty": "合集为空",
"unknownLibrarySection": "无法删除:未知的媒体库分区",
"deleteCollection": "删除合集",
"deleteConfirm": "确定要删除\"${title}\"吗?此操作无法撤销。",
"deleted": "已删除合集",
"deleteFailed": "删除合集失败",
"deleteFailedWithError": "删除合集失败:${error}",
"failedToLoadItems": "加载合集项目失败:${error}",
"selectCollection": "选择合集",
"collectionName": "合集名称",
"enterCollectionName": "输入合集名称",
"addedToCollection": "已添加到合集",
"errorAddingToCollection": "添加到合集失败",
"created": "已创建合集",
"removeFromCollection": "从合集移除",
"removeFromCollectionConfirm": "将“${title}”从此合集移除?",
"removedFromCollection": "已从合集移除",
"removeFromCollectionFailed": "从合集移除失败",
"removeFromCollectionError": "从合集移除时出错:${error}",
"searchCollections": "搜索合集..."
},
"playlists": {
"title": "播放列表",
"playlist": "播放列表",
"noPlaylists": "未找到播放列表",
"create": "创建播放列表",
"playlistName": "播放列表名称",
@@ -629,38 +657,12 @@
"itemAdded": "已添加到播放列表",
"itemRemoved": "已从播放列表中移除",
"selectPlaylist": "选择播放列表",
"createNewPlaylist": "创建新播放列表",
"errorCreating": "创建播放列表失败",
"errorDeleting": "删除播放列表失败",
"errorLoading": "加载播放列表失败",
"errorAdding": "添加到播放列表失败",
"errorReordering": "重新排序播放列表项目失败",
"errorRemoving": "从播放列表中移除失败",
"playlist": "播放列表"
},
"collections": {
"title": "合集",
"collection": "合集",
"empty": "合集为空",
"unknownLibrarySection": "无法删除:未知的媒体库分区",
"deleteCollection": "删除合集",
"deleteConfirm": "确定要删除\"${title}\"吗?此操作无法撤销。",
"deleted": "已删除合集",
"deleteFailed": "删除合集失败",
"deleteFailedWithError": "删除合集失败:${error}",
"failedToLoadItems": "加载合集项目失败:${error}",
"selectCollection": "选择合集",
"createNewCollection": "创建新合集",
"collectionName": "合集名称",
"enterCollectionName": "输入合集名称",
"addedToCollection": "已添加到合集",
"errorAddingToCollection": "添加到合集失败",
"created": "已创建合集",
"removeFromCollection": "从合集移除",
"removeFromCollectionConfirm": "将“${title}”从此合集移除?",
"removedFromCollection": "已从合集移除",
"removeFromCollectionFailed": "从合集移除失败",
"removeFromCollectionError": "从合集移除时出错:${error}"
"errorRemoving": "从播放列表中移除失败"
},
"watchTogether": {
"title": "一起看",
@@ -697,18 +699,76 @@
"leave": "离开",
"syncing": "同步中...",
"joinWatchSession": "加入观看会话",
"enterCodeHint": "输入8位代码",
"enterCodeHint": "输入5位代码",
"pasteFromClipboard": "从剪贴板粘贴",
"pleaseEnterCode": "请输入会话代码",
"codeMustBe8Chars": "会话代码必须是8个字符",
"codeMustBe5Chars": "会话代码必须是5个字符",
"joinInstructions": "输入主持人分享的会话代码以加入他们的观看会话。",
"failedToCreate": "创建会话失败",
"failedToJoin": "加入会话失败",
"sessionCodeCopied": "会话代码已复制到剪贴板",
"relayUnreachable": "无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。",
"reconnectingToHost": "正在重新连接到主持人...",
"currentPlayback": "当前播放",
"joinCurrentPlayback": "加入当前播放",
"joinCurrentPlaybackDescription": "回到房主当前正在观看的内容",
"failedToOpenCurrentPlayback": "无法打开当前播放",
"participantJoined": "${name} 加入了",
"participantLeft": "${name} 离开了"
"participantLeft": "${name} 离开了",
"participantPaused": "${name} 暂停了",
"participantResumed": "${name} 继续播放了",
"participantSeeked": "${name} 跳转了",
"participantBuffering": "${name} 正在缓冲",
"waitingForParticipants": "等待其他人加载...",
"recentRooms": "最近的房间",
"renameRoom": "重命名房间",
"removeRoom": "移除"
},
"downloads": {
"title": "下载",
"manage": "管理",
"tvShows": "电视剧",
"movies": "电影",
"noDownloads": "暂无下载",
"noDownloadsDescription": "下载的内容将在此处显示以供离线观看",
"downloadNow": "下载",
"deleteDownload": "删除下载",
"retryDownload": "重试下载",
"downloadQueued": "下载已排队",
"serverErrorBitrate": "服务器错误 — 文件可能超出远程流媒体比特率限制",
"episodesQueued": "${count} 集已加入下载队列",
"downloadDeleted": "下载已删除",
"deleteConfirm": "确定要删除 \"${title}\" 吗?下载的文件将从您的设备中删除。",
"deletingWithProgress": "正在删除 ${title}... (${current}/${total})",
"noDownloadsTree": "暂无下载",
"pauseAll": "全部暂停",
"resumeAll": "全部继续",
"deleteAll": "全部删除",
"selectVersion": "选择版本",
"allEpisodes": "所有剧集",
"unwatchedOnly": "仅未观看",
"nextNUnwatched": "接下来 ${count} 集未观看",
"customAmount": "自定义数量...",
"howManyEpisodes": "下载几集?",
"itemsQueued": "${count} 个项目已加入下载队列",
"keepSynced": "保持同步",
"downloadOnce": "下载一次",
"keepNUnwatched": "保留${count}个未观看",
"editSyncRule": "编辑同步规则",
"removeSyncRule": "删除同步规则",
"removeSyncRuleConfirm": "停止同步“${title}”?已下载的剧集将被保留。",
"syncRuleCreated": "同步规则已创建 — 保留${count}个未观看的剧集",
"syncRuleUpdated": "同步规则已更新",
"syncRuleRemoved": "同步规则已删除",
"syncedNewEpisodes": "已为${title}同步${count}个新剧集",
"activeSyncRules": "同步规则",
"noSyncRules": "没有同步规则",
"manageSyncRule": "管理同步",
"editEpisodeCount": "剧集数量",
"editSyncFilter": "同步筛选",
"syncAllItems": "同步所有项目",
"syncUnwatchedItems": "同步未观看项目",
"syncRuleListCreated": "同步规则已创建"
},
"shaders": {
"title": "着色器",
@@ -716,58 +776,46 @@
"nvscalerDescription": "NVIDIA 图像缩放,使视频更清晰",
"qualityFast": "快速",
"qualityHQ": "高质量",
"mode": "模式"
"mode": "模式",
"importShader": "导入着色器",
"customShaderDescription": "自定义 GLSL 着色器",
"shaderImported": "着色器已导入",
"shaderImportFailed": "导入着色器失败",
"deleteShader": "删除着色器",
"deleteShaderConfirm": "删除\"${name}\""
},
"companionRemote": {
"title": "Companion Remote",
"connectToDevice": "连接到设备",
"hostRemoteSession": "创建远程会话",
"controlThisDevice": "使用手机控制此设备",
"remoteControl": "远程控制",
"controlDesktop": "控制桌面设备",
"title": "伴侣遥控",
"connectedTo": "已连接到 ${name}",
"session": {
"creatingSession": "正在创建远程会话...",
"failedToCreate": "创建远程会话失败:",
"noSession": "没有可用的会话",
"scanQrCode": "扫描 QR 码",
"orEnterManually": "或手动输入",
"startingServer": "正在启动远程服务器...",
"failedToCreate": "启动远程服务器失败:",
"hostAddress": "主机地址",
"sessionId": "会话 ID",
"pin": "PIN",
"connected": "已连接",
"waitingForConnection": "等待连接中...",
"serverRunning": "远程服务器已启动",
"serverStopped": "远程服务器已停止",
"serverRunningDescription": "网络上的移动设备可以发现并连接到此应用",
"serverStoppedDescription": "启动服务器以允许移动设备连接",
"usePhoneToControl": "使用移动设备控制此应用",
"copiedToClipboard": "${label}已复制到剪贴板",
"copyToClipboard": "复制到剪贴板",
"newSession": "新建会话",
"startServer": "启动服务器",
"stopServer": "停止服务器",
"minimize": "最小化"
},
"pairing": {
"scan": "扫描",
"manual": "手动",
"pairWithDesktop": "与桌面配对",
"enterSessionDetails": "输入桌面设备上显示的会话信息",
"discoveryDescription": "网络上使用相同Plex账户运行Plezy的设备将自动显示",
"hostAddressHint": "192.168.1.100:48632",
"sessionIdHint": "输入8位会话 ID",
"pinHint": "输入6位 PIN",
"connecting": "连接中...",
"tips": "提示",
"tipDesktop": "在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote",
"tipScan": "使用扫描选项卡扫描桌面上的 QR 码以快速配对",
"tipWifi": "请确保两台设备连接到同一个 WiFi 网络",
"cameraPermissionRequired": "扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。",
"cameraError": "无法启动相机:${error}",
"scanInstruction": "将相机对准桌面上显示的 QR 码",
"invalidQrCode": "无效的 QR 码格式",
"connecting": "正在连接...",
"searchingForDevices": "正在搜索设备...",
"noDevicesFound": "未在网络上找到设备",
"noDevicesHint": "请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上",
"availableDevices": "可用设备",
"manualConnection": "手动连接",
"cryptoInitFailed": "无法初始化安全连接。请确保已登录Plex账户。",
"validationHostRequired": "请输入主机地址",
"validationHostFormat": "格式必须为 IP:端口(例如 192.168.1.100:48632",
"validationSessionIdRequired": "请输入会话 ID",
"validationSessionIdLength": "会话 ID 必须为8个字符",
"validationPinRequired": "请输入 PIN",
"validationPinLength": "PIN 必须为6位数字",
"connectionTimedOut": "连接超时。请检查会话 ID 和 PIN。",
"sessionNotFound": "找不到会话。请检查您的凭据。",
"validationHostFormat": "格式必须为IP:端口(例如 192.168.1.100:48632",
"connectionTimedOut": "连接超时。请确保两台设备在同一网络上。",
"sessionNotFound": "未找到设备。请确保Plezy正在主机上运行。",
"authFailed": "认证失败。请确保两台设备使用相同的Plex账户。",
"failedToConnect": "连接失败:${error}"
},
"remote": {
@@ -775,8 +823,6 @@
"reconnecting": "重新连接中...",
"attemptOf": "第 ${current} 次尝试,共 5 次",
"retryNow": "立即重试",
"connectionError": "连接错误",
"notConnected": "未连接",
"tabRemote": "遥控",
"tabPlay": "播放",
"tabMore": "更多",
@@ -819,13 +865,13 @@
"useExternalPlayer": "使用外部播放器",
"useExternalPlayerDescription": "在外部应用中打开视频,而不是使用内置播放器",
"selectPlayer": "选择播放器",
"customPlayers": "自定义播放器",
"systemDefault": "系统默认",
"addCustomPlayer": "添加自定义播放器",
"playerName": "播放器名称",
"playerCommand": "命令",
"playerPackage": "包名",
"playerUrlScheme": "URL 方案",
"customPlayer": "自定义播放器",
"off": "关闭",
"launchFailed": "无法打开外部播放器",
"appNotInstalled": "${name} 未安装",
@@ -847,8 +893,12 @@
"summary": "简介",
"poster": "海报",
"background": "背景",
"logo": "标志",
"squareArt": "方形图片",
"selectPoster": "选择海报",
"selectBackground": "选择背景",
"selectLogo": "选择标志",
"selectSquareArt": "选择方形图片",
"fromUrl": "从 URL",
"uploadFile": "上传文件",
"enterImageUrl": "输入图片 URL",
@@ -890,6 +940,39 @@
"subtitleMode": "自动选择字幕模式",
"manuallySelected": "手动选择",
"shownWithForeignAudio": "外语音频时显示",
"alwaysEnabled": "始终启用"
"alwaysEnabled": "始终启用",
"tags": "标签",
"addTag": "添加标签",
"genre": "类型",
"director": "导演",
"writer": "编剧",
"producer": "制片人",
"country": "国家",
"collection": "合集",
"label": "标记",
"style": "风格",
"mood": "氛围"
},
"serverTasks": {
"title": "服务器任务",
"failedToLoad": "加载任务失败",
"noTasks": "没有正在运行的任务"
},
"trakt": {
"title": "Trakt",
"connected": "已连接",
"connectedAs": "已以 @${username} 身份连接",
"disconnectConfirm": "断开 Trakt 账户?",
"disconnectConfirmBody": "Plezy 将停止向 Trakt 发送播放事件。您随时可以重新连接。",
"scrobble": "实时 Scrobble",
"scrobbleDescription": "在播放时向 Trakt 发送播放、暂停和停止事件。",
"watchedSync": "同步已观看状态",
"watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。",
"deviceCodeTitle": "在 Trakt 上激活 Plezy",
"deviceCodeBody": "请访问 ${url} 并输入此代码:",
"openTraktActivate": "打开 Trakt 进行激活",
"waitingForAuthorization": "等待授权…",
"codeCopied": "代码已复制",
"connectFailed": "无法连接到 Trakt。请重试。"
}
}
+561 -58
View File
@@ -1,22 +1,29 @@
import 'dart:async';
import 'dart:ui' show AppExitResponse;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'dart:io' show Platform;
import 'dart:io' show Platform, ProcessInfo;
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'screens/main_screen.dart';
import 'screens/auth_screen.dart';
import 'services/storage_service.dart';
import 'services/macos_window_service.dart';
import 'services/native_window_service.dart';
import 'services/fullscreen_state_manager.dart';
import 'services/settings_service.dart';
import 'utils/platform_detector.dart';
import 'services/discord_rpc_service.dart';
import 'services/gamepad_service.dart';
import 'services/trakt/trakt_scrobble_service.dart';
import 'services/trakt/trakt_sync_service.dart';
import 'providers/trakt_account_provider.dart';
import 'providers/user_profile_provider.dart';
import 'providers/multi_server_provider.dart';
import 'providers/server_state_provider.dart';
import 'providers/theme_provider.dart';
import 'providers/settings_provider.dart';
import 'providers/hidden_libraries_provider.dart';
@@ -27,6 +34,7 @@ import 'providers/offline_mode_provider.dart';
import 'providers/offline_watch_provider.dart';
import 'providers/companion_remote_provider.dart';
import 'providers/shader_provider.dart';
import 'utils/snackbar_helper.dart';
import 'watch_together/watch_together.dart';
import 'services/multi_server_manager.dart';
import 'services/offline_watch_sync_service.dart';
@@ -40,14 +48,23 @@ import 'services/download_storage_service.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'services/plex_api_cache.dart';
import 'database/app_database.dart';
import 'screens/video_player_screen.dart';
import 'utils/app_logger.dart';
import 'utils/orientation_helper.dart';
import 'utils/language_codes.dart';
import 'utils/global_key_utils.dart';
import 'utils/watch_state_notifier.dart';
import 'i18n/strings.g.dart';
import 'focus/input_mode_tracker.dart';
import 'focus/key_event_utils.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'utils/navigation_transitions.dart';
import 'utils/log_redaction_manager.dart';
import 'package:package_info_plus/package_info_plus.dart';
const bool _enableSentry = bool.fromEnvironment('ENABLE_SENTRY', defaultValue: false);
const String gitCommit = String.fromEnvironment('GIT_COMMIT');
const String _sentryEnvironment = String.fromEnvironment('SENTRY_ENVIRONMENT');
const String _plexTokenDefine = String.fromEnvironment('PLEX_TOKEN');
// Workaround for Flutter bug #177992: iPadOS 26.1+ misinterprets fake touch events
// at (0,0) as barrier taps, causing modals to dismiss immediately.
@@ -66,10 +83,34 @@ void _absorbZeroOffsetPointerEvent(PointerEvent event) {
}
}
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
_installZeroOffsetPointerGuard(); // Workaround for iPadOS 26.1+ modal dismissal bug
if (_enableSentry) {
final packageInfo = await PackageInfo.fromPlatform();
await SentryFlutter.init((options) {
options.dsn = 'https://6a1a6ef8c72140099b2798973c1bfb2f@bugs.plezy.app/1';
options.release = gitCommit.isNotEmpty
? 'plezy@${gitCommit.substring(0, 7)}'
: 'plezy@${packageInfo.version}+${packageInfo.buildNumber}';
if (_sentryEnvironment.isNotEmpty) options.environment = _sentryEnvironment;
options.tracesSampleRate = 0;
options.attachStacktrace = true;
options.enableAutoSessionTracking = false;
options.recordHttpBreadcrumbs = false;
options.captureNativeFailedRequests = false;
options.beforeSend = _beforeSend;
options.beforeBreadcrumb = _beforeBreadcrumb;
}, appRunner: _bootstrapApp);
return;
}
await _bootstrapApp();
}
Future<void> _bootstrapApp() async {
// Initialize settings first to get saved locale
final settings = await SettingsService.getInstance();
final savedLocale = settings.getAppLocale();
@@ -80,9 +121,14 @@ void main() async {
// Needed for formatting dates in different locales
await initializeDateFormatting(savedLocale.languageCode, null);
// Configure image cache for large libraries
PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000
PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB
// Configure image cache — keep budget modest to leave headroom for Skia decode buffers
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
PaintingBinding.instance.imageCache.maximumSize = 1000;
PaintingBinding.instance.imageCache.maximumSizeBytes = 150 << 20; // 150MB
} else {
PaintingBinding.instance.imageCache.maximumSize = 800;
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; // 100MB
}
// Initialize services in parallel where possible
final futures = <Future<void>>[];
@@ -94,7 +140,7 @@ void main() async {
// Initialize TV detection and PiP service for Android
if (Platform.isAndroid) {
futures.add(TvDetectionService.getInstance());
futures.add(TvDetectionService.getInstance(forceTv: settings.getForceTvMode()));
// Initialize PiP service to listen for PiP state changes
PipService();
}
@@ -102,39 +148,157 @@ void main() async {
// Configure macOS window with custom titlebar (depends on window manager)
futures.add(MacOSWindowService.setupCustomTitlebar());
// Hook Windows native fullscreen callback (no-op elsewhere).
NativeWindowService.initialize();
// Initialize storage service
futures.add(StorageService.getInstance());
// Initialize language codes for track selection
futures.add(LanguageCodes.initialize());
// Wait for all parallel services to complete
await Future.wait(futures);
// Seed Plex token from dart-define (used by screenshot automation)
if (_plexTokenDefine.isNotEmpty) {
final storage = await StorageService.getInstance();
await storage.savePlexToken(_plexTokenDefine);
}
// Initialize logger level based on debug setting
final debugEnabled = settings.getEnableDebugLogging();
setLoggerLevel(debugEnabled);
// Log app version and git commit at startup
final packageInfo = await PackageInfo.fromPlatform();
final commitSuffix = gitCommit.isNotEmpty ? ' (${gitCommit.substring(0, 7)})' : '';
String renderer = '';
if (Platform.isAndroid) {
renderer = ' [${await const MethodChannel('com.plezy/theme').invokeMethod<String>('getRenderer')}]';
}
appLogger.i('Plezy v${packageInfo.version}+${packageInfo.buildNumber}$commitSuffix$renderer');
// Initialize download storage service with settings
await DownloadStorageService.instance.initialize(settings);
// Start global fullscreen state monitoring
FullscreenStateManager().startMonitoring();
// Initialize gamepad service for desktop platforms
// Initialize gamepad service (all platforms — universal_gamepad auto-registers
// and intercepts input events, so we must listen to re-dispatch them)
GamepadService.instance.start();
// Desktop-only services
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
GamepadService.instance.start();
DiscordRPCService.instance.initialize();
}
// Trakt scrobble service (all platforms)
await TraktScrobbleService.instance.initialize();
// DTD service is available for MCP tooling connection if needed
// Register bundled shader licenses
_registerShaderLicenses();
// In release mode, show a colored placeholder instead of a blank/white screen
// when a widget build() throws an unhandled exception.
ErrorWidget.builder = (FlutterErrorDetails details) {
if (kDebugMode) return ErrorWidget(details.exception);
return const ColoredBox(color: Color(0xFF000000));
};
runApp(const MainApp());
}
Breadcrumb? _beforeBreadcrumb(Breadcrumb? breadcrumb, Hint _) {
if (breadcrumb == null) return null;
final message = breadcrumb.message;
final data = breadcrumb.data;
if (message == null && (data == null || data.isEmpty)) return breadcrumb;
if (message != null) breadcrumb.message = LogRedactionManager.redact(message);
if (data != null) breadcrumb.data = data.map((k, v) => MapEntry(k, v is String ? LogRedactionManager.redact(v) : v));
return breadcrumb;
}
FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
// Drop event if user opted out of crash reporting
final instance = SettingsService.instanceOrNull;
if (instance != null && !instance.getCrashReporting()) return null;
// Drop unactionable errors
var exceptions = event.exceptions;
if (exceptions != null) {
bool shouldDrop(SentryException e) {
final v = e.value;
// Windows file-lock errors from cache manager cleanup
if (e.type == 'FileSystemException' && v != null && v.contains('plexImageCache') && v.contains('errno = 32')) {
return true;
}
// Linux without DBus/NetworkManager
if (e.type == 'DBusServiceUnknownException' || (v != null && v.contains('system_bus_socket'))) {
return true;
}
// Device out of disk space
if (v != null &&
(v.contains('SQLITE_FULL') ||
v.contains('No space left on device') ||
v.contains('errno = 112') ||
v.contains('database or disk is full'))) {
return true;
}
// Native HTTP errors from CFNetwork (server errors, not actionable)
if (e.type == 'HTTPClientError') return true;
// Discord RPC errors when Discord is not running
if (e.type == 'DiscordStateException') return true;
return false;
}
if (exceptions.any(shouldDrop)) return null;
// Scrub Plex tokens and server URLs from exception messages
for (final e in exceptions) {
final value = e.value;
if (value != null) {
e.value = LogRedactionManager.redact(value);
}
}
}
// Enrich TimeoutException with operation name + duration as tags/fingerprint.
// value format: "TimeoutException after 0:00:05.000000: <operation> timed out"
if (exceptions != null) {
final timeoutException = exceptions.where((e) => e.type == 'TimeoutException').firstOrNull;
if (timeoutException != null) {
final value = timeoutException.value ?? '';
final colonIdx = value.indexOf(': ');
final message = colonIdx >= 0 ? value.substring(colonIdx + 2) : value;
final operation = message.endsWith(' timed out')
? message.substring(0, message.length - ' timed out'.length)
: null;
final durationMatch = RegExp(r'after (\d+:\d{2}:\d{2}\.\d+)').firstMatch(value);
final tags = event.tags ??= {};
if (operation != null) tags['timeout.operation'] = operation;
if (durationMatch != null) tags['timeout.duration'] = durationMatch.group(1)!;
event.fingerprint = ['TimeoutException', ?operation];
}
}
// Scrub breadcrumb messages and data
final breadcrumbs = event.breadcrumbs;
if (breadcrumbs != null) {
for (final b in breadcrumbs) {
final message = b.message;
final data = b.data;
if (message != null) b.message = LogRedactionManager.redact(message);
if (data != null) b.data = data.map((k, v) => MapEntry(k, v is String ? LogRedactionManager.redact(v) : v));
}
}
return event;
}
void _registerShaderLicenses() {
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
@@ -190,6 +354,7 @@ void _registerShaderLicenses() {
// Global RouteObserver for tracking navigation
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
final rootNavigatorKey = GlobalKey<NavigatorState>();
class MainApp extends StatefulWidget {
const MainApp({super.key});
@@ -205,12 +370,37 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
late final AppDatabase _appDatabase;
late final DownloadManagerService _downloadManager;
late final OfflineWatchSyncService _offlineWatchSyncService;
late final AppLifecycleListener _appLifecycleListener;
StreamSubscription<WatchStateEvent>? _watchStateSubscription;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
Timer? _syncDebounce;
final Set<String> _pendingSyncKeys = <String>{};
bool _isAutoDeleteRunning = false;
bool _lastConnectivityWasWifi = false;
/// Last time server health probes ran from a resume event (cooldown for desktop)
DateTime _lastResumeProbe = DateTime(0);
/// Periodic memory check timer for desktop platforms
Timer? _memoryCheckTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// On desktop, periodically check RSS and evict image cache if too high
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
_memoryCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
final rss = ProcessInfo.currentRss;
if (rss > 1536 * 1024 * 1024) {
// 1.5GB
appLogger.w('RSS high ($rss bytes), evicting image caches');
_evictImageCaches();
}
});
}
_serverManager = MultiServerManager();
_aggregationService = DataAggregationService(_serverManager);
_appDatabase = AppDatabase();
@@ -219,34 +409,168 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
PlexApiCache.initialize(_appDatabase);
_downloadManager = DownloadManagerService(database: _appDatabase, storageService: DownloadStorageService.instance);
_downloadManager.setClientResolver(_serverManager.getClient);
_downloadManager.recoveryFuture = _downloadManager.recoverInterruptedDownloads();
_offlineWatchSyncService = OfflineWatchSyncService(database: _appDatabase, serverManager: _serverManager);
// Trakt sync service (subscribes to WatchStateNotifier, requires serverManager
// to resolve PlexClients for GUID lookups).
TraktSyncService.instance.initialize(serverManager: _serverManager);
_appLifecycleListener = AppLifecycleListener(
onExitRequested: () async {
await _appDatabase.close();
return AppExitResponse.exit;
},
);
// Start in-app review session tracking
InAppReviewService.instance.startSession();
}
@override
void dispose() {
_syncDebounce?.cancel();
_watchStateSubscription?.cancel();
_connectivitySubscription?.cancel();
_memoryCheckTimer?.cancel();
_appLifecycleListener.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
appLogger.w('System memory pressure, evicting image caches');
_evictImageCaches();
}
void _evictImageCaches() {
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
}
/// Fires [_autoDeleteAndSync] on each WiFi/Ethernet reconnect so rules run
/// as soon as the device is back online. Rapid flapping is bounded by the
/// executor's cooldown.
void _startConnectivitySyncTrigger(DownloadProvider downloadProvider) {
Future<void> setup() async {
try {
final initial = await Connectivity().checkConnectivity();
_lastConnectivityWasWifi = _hasWifiOrEthernet(initial);
} catch (e) {
appLogger.w('Initial connectivity read failed, defaulting to false: $e');
_lastConnectivityWasWifi = false;
}
try {
_connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) {
final hasWifi = _hasWifiOrEthernet(results);
final transitioned = hasWifi && !_lastConnectivityWasWifi;
_lastConnectivityWasWifi = hasWifi;
if (transitioned) {
appLogger.d('Connectivity moved onto WiFi/Ethernet — triggering sync pass');
_autoDeleteAndSync(downloadProvider);
}
});
} catch (e) {
appLogger.w('Could not subscribe to connectivity changes: $e');
}
}
setup();
}
static bool _hasWifiOrEthernet(List<ConnectivityResult> results) =>
results.contains(ConnectivityResult.wifi) || results.contains(ConnectivityResult.ethernet);
/// Run auto-delete (if enabled) and then a sync-rule pass.
///
/// When [targetKeys] is non-null, only those rules are re-evaluated
/// (cooldown doesn't apply — targeted runs are always "we know this
/// changed"). When null, every rule runs via the executor, with [force]
/// gating the cooldown: `true` for user-initiated drains, `false` for
/// background probes like a connectivity reconnect.
Future<void> _autoDeleteAndSync(
DownloadProvider downloadProvider, {
List<String>? targetKeys,
bool force = false,
}) async {
if (_isAutoDeleteRunning) return;
_isAutoDeleteRunning = true;
try {
await downloadProvider.refreshMetadataFromCache();
final activeKey = VideoPlayerScreenState.activeRatingKey;
final settings = SettingsService.instanceOrNull;
if (settings != null && settings.getAutoRemoveWatchedDownloads()) {
final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey);
if (deleted.isNotEmpty) {
final msg = deleted.length == 1
? t.messages.autoRemovedWatchedDownload(title: deleted.first)
: t.messages.autoRemovedWatchedDownload(title: '${deleted.length} items');
showMainSnackBar(msg);
}
}
if (targetKeys != null) {
for (final key in targetKeys) {
if (!downloadProvider.hasSyncRule(key)) continue;
final result = await downloadProvider.executeSyncRuleFor(key, _serverManager);
if (result != null && result.queuedCount > 0) {
final title = result.title ?? 'Unknown';
showMainSnackBar(t.downloads.syncedNewEpisodes(count: '1', title: '$title (${result.queuedCount})'));
}
}
} else {
final synced = await downloadProvider.executeSyncRules(_serverManager, force: force);
if (synced.isNotEmpty) {
showMainSnackBar(t.downloads.syncedNewEpisodes(count: synced.length.toString(), title: synced.first));
}
}
} finally {
_isAutoDeleteRunning = false;
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - trigger sync check and start new session
_offlineWatchSyncService.onAppResumed();
TraktSyncService.instance.flushQueue();
InAppReviewService.instance.startSession();
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep
_serverManager.checkServerHealth();
_serverManager.reconnectOfflineServers();
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep.
// On desktop, resumed fires on every window focus (alt-tab), so apply a cooldown
// to avoid piling up network probes from rapid alt-tabbing.
final now = DateTime.now();
final cooldown = (Platform.isIOS || Platform.isAndroid)
? const Duration(seconds: 10)
: const Duration(minutes: 2);
if (now.difference(_lastResumeProbe) >= cooldown) {
_lastResumeProbe = now;
// Await health check before reconnecting so stale "online" servers
// get marked offline and included in the reconnection sweep.
unawaited(() async {
await _serverManager.checkServerHealth();
await _serverManager.reconnectOfflineServers();
}());
}
case AppLifecycleState.paused:
case AppLifecycleState.detached:
// App went to background or is closing - end session
// Database is session-scoped and must survive suspend/resume.
// Closing here would kill the Drift isolate channel while services
// (sync, downloads, cache) still hold references to the executor.
// SQLite WAL mode handles process death; desktop uses onExitRequested.
InAppReviewService.instance.endSession();
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
if (ProcessInfo.currentRss > 1024 * 1024 * 1024) {
// 1GB
_evictImageCaches();
}
}
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
// Transitional states - don't trigger session events
@@ -259,7 +583,6 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MultiServerProvider(_serverManager, _aggregationService)),
ChangeNotifierProvider(create: (context) => ServerStateProvider()),
// Offline mode provider - depends on MultiServerProvider
ChangeNotifierProxyProvider<MultiServerProvider, OfflineModeProvider>(
create: (_) {
@@ -274,18 +597,44 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
},
),
// Download provider
ChangeNotifierProvider(create: (context) => DownloadProvider(downloadManager: _downloadManager)),
ChangeNotifierProvider(
create: (context) => DownloadProvider(downloadManager: _downloadManager, database: _appDatabase),
),
// Offline watch sync service
ChangeNotifierProvider<OfflineWatchSyncService>(
create: (context) {
final offlineModeProvider = context.read<OfflineModeProvider>();
final downloadProvider = context.read<DownloadProvider>();
// Wire up callback to refresh download provider after watch state sync
_offlineWatchSyncService.onWatchStatesRefreshed = () {
downloadProvider.refreshMetadataFromCache();
// Offline-sync drain replays a batch of queued watch actions without
// per-item data, so we can't target rules — force a full pass.
_offlineWatchSyncService.onWatchStatesRefreshed = () async {
await _autoDeleteAndSync(downloadProvider, force: true);
};
// In-session watch events carry the episode's parent chain, so we
// only re-evaluate rules that actually cover the watched item —
// leaves unrelated collection/playlist rules alone. Debounced so
// binge-watching coalesces into one pass.
_watchStateSubscription = WatchStateNotifier().stream.listen((event) {
if (event.changeType != WatchStateChangeType.watched) return;
if (VideoPlayerScreenState.activeRatingKey == event.ratingKey) return;
_pendingSyncKeys.add(event.globalKey);
for (final parentKey in event.parentChain) {
_pendingSyncKeys.add(buildGlobalKey(event.serverId, parentKey));
}
_syncDebounce?.cancel();
_syncDebounce = Timer(const Duration(seconds: 5), () {
final keys = _pendingSyncKeys.toList();
_pendingSyncKeys.clear();
_autoDeleteAndSync(downloadProvider, targetKeys: keys);
});
});
_startConnectivitySyncTrigger(downloadProvider);
_offlineWatchSyncService.startConnectivityMonitoring(offlineModeProvider);
return _offlineWatchSyncService;
},
@@ -295,20 +644,17 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
create: (context) => OfflineWatchProvider(
syncService: _offlineWatchSyncService,
downloadProvider: context.read<DownloadProvider>(),
apiCache: PlexApiCache.instance,
),
update: (_, syncService, downloadProvider, previous) {
return previous ??
OfflineWatchProvider(
syncService: syncService,
downloadProvider: downloadProvider,
apiCache: PlexApiCache.instance,
);
return previous ?? OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
},
),
// Existing providers
ChangeNotifierProvider(create: (context) => UserProfileProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
// Trakt account — depends on UserProfileProvider for per-profile token scoping.
// Hydrated and rebound in `_TraktBootstrap` below.
ChangeNotifierProvider(create: (context) => TraktAccountProvider()),
ChangeNotifierProvider(create: (context) => SettingsProvider(), lazy: true),
ChangeNotifierProvider(create: (context) => HiddenLibrariesProvider(), lazy: true),
ChangeNotifierProvider(create: (context) => LibrariesProvider()),
@@ -320,15 +666,30 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return TranslationProvider(
child: InputModeTracker(
child: MaterialApp(
title: t.app.title,
debugShowCheckedModeBanner: false,
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.materialThemeMode,
navigatorObservers: [routeObserver, BackKeySuppressorObserver()],
home: const OrientationAwareSetup(),
child: _TraktProfileBootstrap(
child: Listener(
onPointerDown: (event) {
if ((event.buttons & kBackMouseButton) != 0) {
rootNavigatorKey.currentState?.maybePop();
}
},
behavior: HitTestBehavior.translucent,
child: InputModeTracker(
child: MaterialApp(
title: t.app.title,
debugShowCheckedModeBanner: false,
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.materialThemeMode,
navigatorKey: rootNavigatorKey,
navigatorObservers: [routeObserver, BackKeySuppressorObserver()],
home: const OrientationAwareSetup(),
builder: (context, child) => ScaffoldMessenger(
key: rootScaffoldMessengerKey,
child: Scaffold(backgroundColor: Colors.transparent, body: child),
),
),
),
),
),
);
@@ -338,6 +699,61 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
}
}
/// Hydrates the [TraktAccountProvider] with the active Plex profile's session
/// and rebinds the scrobble + sync services whenever the user switches.
///
/// Lives high in the widget tree (above MaterialApp) so the listener survives
/// route changes.
class _TraktProfileBootstrap extends StatefulWidget {
final Widget child;
const _TraktProfileBootstrap({required this.child});
@override
State<_TraktProfileBootstrap> createState() => _TraktProfileBootstrapState();
}
class _TraktProfileBootstrapState extends State<_TraktProfileBootstrap> {
UserProfileProvider? _profile;
TraktAccountProvider? _trakt;
String? _lastUuid;
bool _hydrated = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final profile = context.read<UserProfileProvider>();
final trakt = context.read<TraktAccountProvider>();
if (!identical(_profile, profile)) {
_profile?.removeListener(_onProfileChanged);
_profile = profile;
_profile!.addListener(_onProfileChanged);
}
_trakt = trakt;
if (!_hydrated) {
_hydrated = true;
_onProfileChanged();
}
}
void _onProfileChanged() {
final uuid = _profile?.currentUser?.uuid;
if (uuid == _lastUuid) return;
_lastUuid = uuid;
_trakt?.onActiveProfileChanged(uuid);
}
@override
void dispose() {
_profile?.removeListener(_onProfileChanged);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}
class OrientationAwareSetup extends StatefulWidget {
const OrientationAwareSetup({super.key});
@@ -372,6 +788,9 @@ class SetupScreen extends StatefulWidget {
class _SetupScreenState extends State<SetupScreen> {
String _statusMessage = '';
// Per-server connection status: serverId -> (name, connected?)
final Map<String, (String name, bool? connected)> _serverStatus = {};
@override
void initState() {
super.initState();
@@ -390,11 +809,20 @@ class _SetupScreenState extends State<SetupScreen> {
// Check network connectivity early to fast-path airplane mode.
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3),
onTimeout: () => [ConnectivityResult.other],
);
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
bool hasNetwork;
Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup'));
try {
final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3),
onTimeout: () => [ConnectivityResult.other],
);
hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
} catch (e) {
// connectivity_plus throws DBusServiceUnknownException on Linux without NetworkManager
hasNetwork = true;
}
Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup'));
if (hasNetwork) {
_setStatus(t.common.refreshingServers);
@@ -435,8 +863,18 @@ class _SetupScreenState extends State<SetupScreen> {
return;
}
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup'));
_setStatus(t.common.connectingToServers);
// Populate per-server status for splash display
if (mounted) {
setState(() {
for (final server in servers) {
_serverStatus[server.clientIdentifier] = (server.name, null);
}
});
}
try {
final result = await ServerConnectionOrchestrator.connectAndInitialize(
servers: servers,
@@ -444,11 +882,21 @@ class _SetupScreenState extends State<SetupScreen> {
librariesProvider: context.read<LibrariesProvider>(),
syncService: context.read<OfflineWatchSyncService>(),
clientIdentifier: storage.getClientIdentifier(),
onServerStatus: (serverId, success) {
if (mounted) {
setState(() {
final existing = _serverStatus[serverId];
if (existing != null) {
_serverStatus[serverId] = (existing.$1, success);
}
});
}
},
);
if (!mounted) return;
if (result.hasConnections) {
if (result.hasConnections && result.firstClient != null) {
// Resume any downloads that were interrupted by app kill
final downloadProvider = context.read<DownloadProvider>();
downloadProvider.ensureInitialized().then((_) {
@@ -474,30 +922,85 @@ class _SetupScreenState extends State<SetupScreen> {
}
}
Widget _buildStatusText(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_statusMessage,
key: ValueKey(_statusMessage),
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
),
);
}
Widget _buildServerStatusList(BuildContext context) {
if (_serverStatus.isEmpty) return const SizedBox.shrink();
final textTheme = Theme.of(context).textTheme;
final dimColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5);
const coralColor = Color(0xFFE5A00D);
const successColor = Color(0xFF4CAF50);
const failColor = Color(0xFFEF5350);
return Column(
mainAxisSize: MainAxisSize.min,
children: _serverStatus.entries.map((entry) {
final (name, connected) = entry.value;
final Widget statusIcon;
if (connected == null) {
statusIcon = const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 1.5, color: coralColor),
);
} else if (connected) {
statusIcon = const Icon(Icons.check_circle, size: 14, color: successColor);
} else {
statusIcon = const Icon(Icons.cancel, size: 14, color: failColor);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
statusIcon,
const SizedBox(width: 8),
Text(name, style: textTheme.bodySmall?.copyWith(color: dimColor)),
],
),
);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
const coralColor = Color(0xFFE5A00D);
return ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
child: Stack(
children: [
// Icon dead-center, matching Android 12+ splash position.
// 192dp accounts for the 16% inset in ic_launcher.xml.
Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)),
// Status text below center, independent of icon position.
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height * 0.5 - 140,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_statusMessage,
key: ValueKey(_statusMessage),
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
),
bottom: MediaQuery.of(context).size.height * 0.5 - 170,
child: _buildStatusText(context),
),
Positioned(
left: 0,
right: 0,
top: MediaQuery.of(context).size.height * 0.5 + 180,
child: Center(
child: _serverStatus.isEmpty
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: coralColor),
)
: _buildServerStatusList(context),
),
),
],
+22
View File
@@ -40,6 +40,28 @@ mixin GridFocusNodeMixin<T extends StatefulWidget> on State<T> {
}
}
/// Evict focus nodes far from [centerIndex], keeping at most [keepCount] around it.
void evictDistantFocusNodes(int centerIndex, {int keepCount = 200}) {
if (gridItemFocusNodes.length <= keepCount) return;
final halfKeep = keepCount ~/ 2;
final keepStart = centerIndex - halfKeep;
final keepEnd = centerIndex + halfKeep;
final keysToRemove = <int>[];
for (final key in gridItemFocusNodes.keys) {
if (key < keepStart || key > keepEnd) {
keysToRemove.add(key);
}
}
for (final key in keysToRemove) {
final node = gridItemFocusNodes.remove(key);
if (node != null && !node.hasFocus) {
node.dispose();
}
}
}
/// Dispose all grid-item focus nodes.
void disposeGridFocusNodes() {
for (final node in gridItemFocusNodes.values) {
+10
View File
@@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
/// Provides a safe [setState] wrapper for async callbacks.
mixin MountedSetStateMixin<T extends StatefulWidget> on State<T> {
void setStateIfMounted(VoidCallback fn) {
if (mounted) {
setState(fn);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:flutter/widgets.dart';
import '../models/plex_metadata.dart';
import '../services/plex_client.dart';
import '../utils/global_key_utils.dart';
import '../utils/provider_extensions.dart';
/// Shared helpers for screens bound to a single [PlexMetadata] item/server.
mixin ServerBoundMediaMixin<T extends StatefulWidget> on State<T> {
PlexMetadata get serverBoundMetadata;
bool get isServerBoundOffline => false;
String? get serverBoundServerId => serverBoundMetadata.serverId;
String toServerBoundGlobalKey(String ratingKey, {String? serverId}) =>
buildGlobalKey(serverId ?? serverBoundServerId ?? '', ratingKey);
PlexClient? getServerBoundClient(BuildContext context) =>
context.getClientForMetadataOrNull(serverBoundMetadata, isOffline: isServerBoundOffline);
}
+49 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../services/gamepad_service.dart';
import '../screens/main_screen.dart';
import '../widgets/focusable_tab_chip.dart';
/// Mixin that provides common tab navigation infrastructure.
///
@@ -12,7 +13,7 @@ import '../screens/main_screen.dart';
/// - Tab bar back navigation to sidebar
///
/// Subclasses must provide [tabChipFocusNodes] — one [FocusNode] per tab.
mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, SingleTickerProviderStateMixin<T> {
mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, TickerProviderStateMixin<T> {
late TabController tabController;
/// When true, suppress auto-focus in tabs (used when navigating via tab bar).
@@ -83,4 +84,51 @@ mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, SingleTickerProv
void onTabBarBack() {
MainScreenFocusScope.of(context)?.focusSidebar();
}
/// Shared tab chip builder — eliminates duplication between screens.
Widget buildTabChip(
String label,
int index, {
required VoidCallback onSelectWhenActive,
required VoidCallback onNavigateDown,
VoidCallback? onNavigateRightFromLast,
}) {
final isSelected = tabController.index == index;
return FocusableTabChip(
label: label,
isSelected: isSelected,
focusNode: getTabChipFocusNode(index),
onSelect: () {
if (isSelected) {
onSelectWhenActive();
} else {
setState(() {
tabController.index = index;
});
}
},
onNavigateLeft: index > 0
? () {
final newIndex = index - 1;
setState(() {
suppressAutoFocus = true;
tabController.index = newIndex;
});
getTabChipFocusNode(newIndex).requestFocus();
}
: onTabBarBack,
onNavigateRight: index < tabCount - 1
? () {
final newIndex = index + 1;
setState(() {
suppressAutoFocus = true;
tabController.index = newIndex;
});
getTabChipFocusNode(newIndex).requestFocus();
}
: onNavigateRightFromLast,
onNavigateDown: onNavigateDown,
onBack: onTabBarBack,
);
}
}
@@ -56,8 +56,6 @@ class RemoteDevice {
@JsonSerializable()
class RemoteSession {
final String sessionId;
final String pin;
@JsonKey(unknownEnumValue: RemoteSessionRole.remote)
final RemoteSessionRole role;
@JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected)
@@ -67,8 +65,6 @@ class RemoteSession {
final String? errorMessage;
RemoteSession({
required this.sessionId,
required this.pin,
required this.role,
this.status = RemoteSessionStatus.disconnected,
this.connectedDevice,
@@ -85,8 +81,6 @@ class RemoteSession {
Map<String, dynamic> toJson() => _$RemoteSessionToJson(this);
RemoteSession copyWith({
String? sessionId,
String? pin,
RemoteSessionRole? role,
RemoteSessionStatus? status,
RemoteDevice? connectedDevice,
@@ -96,8 +90,6 @@ class RemoteSession {
bool clearErrorMessage = false,
}) {
return RemoteSession(
sessionId: sessionId ?? this.sessionId,
pin: pin ?? this.pin,
role: role ?? this.role,
status: status ?? this.status,
connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice),
@@ -10,47 +10,61 @@ RemoteDevice _$RemoteDeviceFromJson(Map<String, dynamic> json) => RemoteDevice(
id: json['id'] as String,
name: json['name'] as String,
platform: json['platform'] as String,
connectedAt: json['connectedAt'] == null ? null : DateTime.parse(json['connectedAt'] as String),
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map((k, e) => MapEntry(k, e as bool)),
);
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'platform': instance.platform,
'connectedAt': instance.connectedAt.toIso8601String(),
'capabilities': instance.capabilities,
};
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession(
sessionId: json['sessionId'] as String,
pin: json['pin'] as String,
role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote),
status:
$enumDecodeNullable(
_$RemoteSessionStatusEnumMap,
json['status'],
unknownValue: RemoteSessionStatus.disconnected,
) ??
RemoteSessionStatus.disconnected,
connectedDevice: json['connectedDevice'] == null
connectedAt: json['connectedAt'] == null
? null
: RemoteDevice.fromJson(json['connectedDevice'] as Map<String, dynamic>),
createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String),
errorMessage: json['errorMessage'] as String?,
: DateTime.parse(json['connectedAt'] as String),
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as bool),
),
);
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => <String, dynamic>{
'sessionId': instance.sessionId,
'pin': instance.pin,
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice,
'createdAt': instance.createdAt.toIso8601String(),
'errorMessage': instance.errorMessage,
};
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'platform': instance.platform,
'connectedAt': instance.connectedAt.toIso8601String(),
'capabilities': instance.capabilities,
};
const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'};
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
RemoteSession(
role: $enumDecode(
_$RemoteSessionRoleEnumMap,
json['role'],
unknownValue: RemoteSessionRole.remote,
),
status:
$enumDecodeNullable(
_$RemoteSessionStatusEnumMap,
json['status'],
unknownValue: RemoteSessionStatus.disconnected,
) ??
RemoteSessionStatus.disconnected,
connectedDevice: json['connectedDevice'] == null
? null
: RemoteDevice.fromJson(
json['connectedDevice'] as Map<String, dynamic>,
),
createdAt: json['createdAt'] == null
? null
: DateTime.parse(json['createdAt'] as String),
errorMessage: json['errorMessage'] as String?,
);
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
<String, dynamic>{
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice,
'createdAt': instance.createdAt.toIso8601String(),
'errorMessage': instance.errorMessage,
};
const _$RemoteSessionRoleEnumMap = {
RemoteSessionRole.host: 'host',
RemoteSessionRole.remote: 'remote',
};
const _$RemoteSessionStatusEnumMap = {
RemoteSessionStatus.disconnected: 'disconnected',
-7
View File
@@ -41,13 +41,6 @@ class DownloadProgress {
String get downloadedFormatted => ByteFormatter.formatBytes(downloadedBytes);
String get totalFormatted => ByteFormatter.formatBytes(totalBytes);
Duration? get estimatedTimeRemaining {
if (speed <= 0 || totalBytes <= 0) return null;
final remainingBytes = totalBytes - downloadedBytes;
if (remainingBytes <= 0) return Duration.zero;
return Duration(seconds: (remainingBytes / speed).round());
}
/// Check if this progress update includes artwork paths
bool get hasArtworkPaths => thumbPath != null;
+3 -87
View File
@@ -2,6 +2,8 @@ import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import '../data/hid_key_labels.dart';
/// Modifier keys that can be combined with a primary key to form a hotkey.
///
/// Each value holds the physical keys that correspond to it (e.g. shift maps
@@ -40,7 +42,7 @@ String physicalKeyLabel(PhysicalKeyboardKey key) {
final macLabel = _macKeyLabels[key.usbHidUsage];
if (macLabel != null) return macLabel;
}
return _knownKeyLabels[key.usbHidUsage] ?? key.debugName ?? 'Unknown';
return hidKeyLabels[key.usbHidUsage] ?? 'Key 0x${key.usbHidUsage.toRadixString(16).padLeft(8, '0')}';
}
/// macOS-specific overrides for keys that have standard symbols.
@@ -69,89 +71,3 @@ const _macKeyLabels = <int, String>{
0x000700e7: '\u2318', // metaRight (Command) → ⌘
0x00000012: 'fn', // fn
};
const _knownKeyLabels = <int, String>{
0x00070004: 'A', // keyA
0x00070005: 'B', // keyB
0x00070006: 'C', // keyC
0x00070007: 'D', // keyD
0x00070008: 'E', // keyE
0x00070009: 'F', // keyF
0x0007000a: 'G', // keyG
0x0007000b: 'H', // keyH
0x0007000c: 'I', // keyI
0x0007000d: 'J', // keyJ
0x0007000e: 'K', // keyK
0x0007000f: 'L', // keyL
0x00070010: 'M', // keyM
0x00070011: 'N', // keyN
0x00070012: 'O', // keyO
0x00070013: 'P', // keyP
0x00070014: 'Q', // keyQ
0x00070015: 'R', // keyR
0x00070016: 'S', // keyS
0x00070017: 'T', // keyT
0x00070018: 'U', // keyU
0x00070019: 'V', // keyV
0x0007001a: 'W', // keyW
0x0007001b: 'X', // keyX
0x0007001c: 'Y', // keyY
0x0007001d: 'Z', // keyZ
0x0007001e: '1', // digit1
0x0007001f: '2', // digit2
0x00070020: '3', // digit3
0x00070021: '4', // digit4
0x00070022: '5', // digit5
0x00070023: '6', // digit6
0x00070024: '7', // digit7
0x00070025: '8', // digit8
0x00070026: '9', // digit9
0x00070027: '0', // digit0
0x00070028: 'Enter', // enter
0x00070029: 'Escape', // escape
0x0007002a: 'Backspace', // backspace
0x0007002b: 'Tab', // tab
0x0007002c: 'Space', // space
0x0007002d: '=', // equal
0x0007002e: '-', // minus
0x0007002f: '[', // bracketLeft
0x00070030: ']', // bracketRight
0x00070031: r'\', // backslash
0x00070033: ';', // semicolon
0x00070034: "'", // quote
0x00070035: '`', // backquote
0x00070036: ',', // comma
0x00070037: '.', // period
0x00070038: '/', // slash
0x00070039: 'CapsLock', // capsLock
0x0007003a: 'F1',
0x0007003b: 'F2',
0x0007003c: 'F3',
0x0007003d: 'F4',
0x0007003e: 'F5',
0x0007003f: 'F6',
0x00070040: 'F7',
0x00070041: 'F8',
0x00070042: 'F9',
0x00070043: 'F10',
0x00070044: 'F11',
0x00070045: 'F12',
0x0007004a: 'Home', // home
0x0007004b: 'Page Up', // pageUp
0x0007004c: 'Delete', // delete
0x0007004d: 'End', // end
0x0007004e: 'Page Down', // pageDown
0x0007004f: 'Arrow Right', // arrowRight
0x00070050: 'Arrow Left', // arrowLeft
0x00070051: 'Arrow Down', // arrowDown
0x00070052: 'Arrow Up', // arrowUp
0x000700e0: 'Ctrl', // controlLeft
0x000700e1: 'Shift', // shiftLeft
0x000700e2: 'Alt', // altLeft
0x000700e3: 'Meta', // metaLeft
0x000700e4: 'Ctrl', // controlRight
0x000700e5: 'Shift', // shiftRight
0x000700e6: 'Alt', // altRight
0x000700e7: 'Meta', // metaRight
0x00000012: 'Fn', // fn
};
+43
View File
@@ -0,0 +1,43 @@
/// Represents the seekable capture buffer for a live TV transcode session.
///
/// Extracted from the `TranscodeSession` element in the tune response:
/// - [startedAt] = `timeStamp` (epoch seconds, the buffer origin)
/// - [seekStartSeconds] = `minOffsetAvailable` (earliest seekable offset)
/// - [seekEndSeconds] = `maxOffsetAvailable` (latest seekable offset / live edge)
class CaptureBuffer {
final double startedAt;
final double seekStartSeconds;
final double seekEndSeconds;
const CaptureBuffer({required this.startedAt, required this.seekStartSeconds, required this.seekEndSeconds});
/// Absolute epoch second of the earliest seekable point.
int get seekableStartEpoch => (startedAt + seekStartSeconds).round();
/// Absolute epoch second of the latest seekable point (≈ live edge).
int get seekableEndEpoch => (startedAt + seekEndSeconds).round();
/// Duration of the seekable range in seconds.
int get seekableDurationSeconds => (seekEndSeconds - seekStartSeconds).round();
/// Parse from a TranscodeSession JSON map. Returns null if required fields are missing.
/// Values may be num or String depending on whether the server returned JSON or XML.
static CaptureBuffer? fromTranscodeSession(Map<String, dynamic> session) {
final timeStamp = _parseDouble(session['timeStamp']);
final minOffset = _parseDouble(session['minOffsetAvailable']);
final maxOffset = _parseDouble(session['maxOffsetAvailable']);
if (timeStamp == null || minOffset == null || maxOffset == null) return null;
return CaptureBuffer(startedAt: timeStamp, seekStartSeconds: minOffset, seekEndSeconds: maxOffset);
}
static double? _parseDouble(dynamic value) {
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value);
return null;
}
@override
String toString() =>
'CaptureBuffer(startedAt: $startedAt, seek: $seekStartSeconds..$seekEndSeconds, '
'range: $seekableStartEpoch..$seekableEndEpoch)';
}
+58 -5
View File
@@ -1,3 +1,5 @@
import '../utils/json_utils.dart';
/// Represents a Live TV channel from the EPG
class LiveTvChannel {
final String key;
@@ -34,17 +36,27 @@ class LiveTvChannel {
factory LiveTvChannel.fromJson(Map<String, dynamic> json) {
return LiveTvChannel(
key: json['key'] as String? ?? json['ratingKey'] as String? ?? json['identifier'] as String? ?? json['channelIdentifier'] as String? ?? '',
identifier: json['identifier'] as String? ?? json['channelIdentifier'] as String?,
key:
json['key'] as String? ??
json['ratingKey'] as String? ??
json['identifier'] as String? ??
json['id'] as String? ??
json['channelIdentifier'] as String? ??
'',
identifier: json['identifier'] as String? ?? json['id'] as String? ?? json['channelIdentifier'] as String?,
callSign: json['callSign'] as String?,
title: json['title'] as String? ?? json['callSign'] as String?,
thumb: json['thumb'] as String?,
art: json['art'] as String?,
number: json['number'] as String? ?? json['channelNumber'] as String? ?? json['channelVcn']?.toString(),
hd: json['hd'] == true || json['hd'] == 1,
number:
json['number'] as String? ??
json['channelNumber'] as String? ??
json['channelVcn']?.toString() ??
json['vcn']?.toString(),
hd: flexibleBool(json['hd']),
lineup: json['lineup'] as String?,
slug: json['slug'] as String?,
drm: json['drm'] == true || json['drm'] == 1,
drm: flexibleBool(json['drm']),
);
}
@@ -69,3 +81,44 @@ class LiveTvChannel {
/// Display name: prefer callSign, fallback to title
String get displayName => callSign ?? title ?? 'Channel $number';
}
/// A channel entry in the Plex cloud favorites list.
/// Stored at `https://epg.provider.plex.tv/settings/favoriteChannels`.
class FavoriteChannel {
final String source;
final String id;
final String? title;
final String? thumb;
final String? vcn;
FavoriteChannel({required this.source, required this.id, this.title, this.thumb, this.vcn});
factory FavoriteChannel.fromJson(Map<String, dynamic> json) {
return FavoriteChannel(
source: json['source'] as String? ?? '',
id: json['id'] as String? ?? json['key'] as String? ?? '',
title: json['title'] as String?,
thumb: json['thumb'] as String?,
vcn: json['vcn'] as String?,
);
}
Map<String, dynamic> toJson() => {
'source': source,
'id': id,
if (title != null) 'title': title,
if (thumb != null) 'thumb': thumb,
if (vcn != null) 'vcn': vcn,
};
/// Create from a [LiveTvChannel] and a source URI.
factory FavoriteChannel.fromLiveTvChannel(LiveTvChannel channel, String source) {
return FavoriteChannel(
source: source,
id: channel.key,
title: channel.title ?? channel.callSign,
thumb: channel.thumb,
vcn: channel.number,
);
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
import '../utils/json_utils.dart';
/// Represents a Plex Live TV DVR device (e.g., HDHomeRun tuner, IPTV provider)
class LiveTvDvr {
final String key;
@@ -74,7 +76,7 @@ class ChannelMapping {
return ChannelMapping(
channelKey: json['channelKey'] as String?,
deviceIdentifier: json['deviceIdentifier'] as String?,
enabled: json['enabled'] == true || json['enabled'] == 1 || json['enabled'] == '1',
enabled: flexibleBool(json['enabled']),
lineupIdentifier: json['lineupIdentifier'] as String?,
);
}
+27 -10
View File
@@ -1,3 +1,5 @@
import '../utils/json_utils.dart';
/// Represents an EPG program entry (what's on a channel at a given time)
class LiveTvProgram {
final String? key;
@@ -42,11 +44,27 @@ class LiveTvProgram {
this.premiere,
});
factory LiveTvProgram.fromJson(Map<String, dynamic> json) {
// Grid endpoint nests timing/channel info inside Media[0] and Channel[0]
final media = (json['Media'] as List?)?.firstOrNull as Map<String, dynamic>?;
factory LiveTvProgram.fromJson(Map<String, dynamic> json, {Map<String, dynamic>? mediaOverride}) {
// Grid endpoint nests timing/channel info inside Media[] and Channel[].
// When mediaOverride is supplied, the caller is pinning this parse to a
// specific airing (one Media entry); treat it as authoritative for
// begin/end/channel fields.
final hasOverride = mediaOverride != null;
final media = mediaOverride ?? (json['Media'] as List?)?.firstOrNull as Map<String, dynamic>?;
final channel = (json['Channel'] as List?)?.firstOrNull as Map<String, dynamic>?;
int? pickInt(String key) {
final fromMedia = (media?[key] as num?)?.toInt();
final fromJson = (json[key] as num?)?.toInt();
return hasOverride ? (fromMedia ?? fromJson) : (fromJson ?? fromMedia);
}
String? pickString(String key) {
final fromMedia = media?[key]?.toString();
final fromJson = json[key] as String?;
return hasOverride ? (fromMedia ?? fromJson) : (fromJson ?? fromMedia);
}
return LiveTvProgram(
key: json['key'] as String?,
ratingKey: json['ratingKey'] as String?,
@@ -55,19 +73,18 @@ class LiveTvProgram {
summary: json['summary'] as String?,
type: json['type'] as String?,
year: (json['year'] as num?)?.toInt(),
beginsAt: (json['beginsAt'] as num?)?.toInt() ?? (media?['beginsAt'] as num?)?.toInt(),
endsAt: (json['endsAt'] as num?)?.toInt() ?? (media?['endsAt'] as num?)?.toInt(),
beginsAt: pickInt('beginsAt'),
endsAt: pickInt('endsAt'),
grandparentTitle: json['grandparentTitle'] as String?,
parentTitle: json['parentTitle'] as String?,
index: (json['index'] as num?)?.toInt(),
parentIndex: (json['parentIndex'] as num?)?.toInt(),
thumb: json['thumb'] as String? ?? json['grandparentThumb'] as String?,
art: json['art'] as String?,
channelIdentifier:
json['channelIdentifier'] as String? ?? media?['channelIdentifier']?.toString() ?? channel?['id']?.toString(),
channelCallSign: json['channelCallSign'] as String? ?? media?['channelCallSign'] as String?,
live: json['live'] == true || json['live'] == 1 || json['live'] == '1',
premiere: json['premiere'] == true || json['premiere'] == 1 || json['premiere'] == '1',
channelIdentifier: pickString('channelIdentifier') ?? channel?['id']?.toString(),
channelCallSign: pickString('channelCallSign'),
live: flexibleBool(json['live']),
premiere: flexibleBool(json['premiere']),
);
}
+4 -31
View File
@@ -1,45 +1,18 @@
/// Represents a single MPV configuration entry
class MpvConfigEntry {
final String key;
final String value;
final bool isEnabled;
const MpvConfigEntry({required this.key, required this.value, this.isEnabled = true});
factory MpvConfigEntry.fromJson(Map<String, dynamic> json) {
return MpvConfigEntry(
key: json['key'] as String,
value: json['value'] as String,
isEnabled: json['isEnabled'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() => {'key': key, 'value': value, 'isEnabled': isEnabled};
MpvConfigEntry copyWith({String? key, String? value, bool? isEnabled}) {
return MpvConfigEntry(key: key ?? this.key, value: value ?? this.value, isEnabled: isEnabled ?? this.isEnabled);
}
}
/// Represents a saved preset of MPV configurations
class MpvPreset {
final String name;
final List<MpvConfigEntry> entries;
final String text;
final DateTime createdAt;
const MpvPreset({required this.name, required this.entries, required this.createdAt});
const MpvPreset({required this.name, required this.text, required this.createdAt});
factory MpvPreset.fromJson(Map<String, dynamic> json) {
return MpvPreset(
name: json['name'] as String,
entries: (json['entries'] as List).map((e) => MpvConfigEntry.fromJson(e as Map<String, dynamic>)).toList(),
text: json['text'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
Map<String, dynamic> toJson() => {
'name': name,
'entries': entries.map((e) => e.toJson()).toList(),
'createdAt': createdAt.toIso8601String(),
};
Map<String, dynamic> toJson() => {'name': name, 'text': text, 'createdAt': createdAt.toIso8601String()};
}

Some files were not shown because too many files have changed in this diff Show More