From 3f40007f03b2935bd6e9e4e8fd98551a7254b446 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 02:16:07 +0100 Subject: [PATCH 01/25] feat: throttle seeking commands --- .../video_controls/video_controls.dart | 212 +++++++++++------- 1 file changed, 130 insertions(+), 82 deletions(-) diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 122cfdf86..b462c664f 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -92,6 +92,9 @@ class _PlexVideoControlsState extends State double _doubleTapFeedbackOpacity = 0.0; bool _lastDoubleTapWasForward = true; Timer? _feedbackTimer; + // Seek throttle state + Timer? _seekThrottleTimer; + Duration? _pendingSeekPosition; @override void initState() { @@ -168,6 +171,7 @@ class _PlexVideoControlsState extends State void dispose() { _hideTimer?.cancel(); _feedbackTimer?.cancel(); + _seekThrottleTimer?.cancel(); _focusNode.dispose(); // Remove lifecycle observer WidgetsBinding.instance.removeObserver(this); @@ -358,11 +362,7 @@ class _PlexVideoControlsState extends State // Only apply SafeArea in portrait mode if (isPortrait) { - return SafeArea( - top: top, - bottom: bottom, - child: child, - ); + return SafeArea(top: top, bottom: bottom, child: child); } // In landscape, return child without SafeArea @@ -380,83 +380,90 @@ class _PlexVideoControlsState extends State mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Unified settings button (speed, sleep timer, audio sync, subtitle sync) - ListenableBuilder( - listenable: SleepTimerService(), - builder: (context, _) { - final sleepTimer = SleepTimerService(); - final isActive = - sleepTimer.isActive || - _audioSyncOffset != 0 || - _subtitleSyncOffset != 0; - return VideoControlButton( - icon: Icons.tune, - isActive: isActive, - onPressed: () async { - await VideoSettingsSheet.show( - context, - widget.player, - _audioSyncOffset, - _subtitleSyncOffset, - ); - // Sheet is now closed, reload immediately - if (mounted) { - await _loadSeekTimes(); - } - }, - ); - }, - ), - if (_hasMultipleAudioTracks(tracks)) - VideoControlButton( - icon: Icons.audiotrack, - onPressed: () => AudioTrackSheet.show(context, widget.player), + // Unified settings button (speed, sleep timer, audio sync, subtitle sync) + ListenableBuilder( + listenable: SleepTimerService(), + builder: (context, _) { + final sleepTimer = SleepTimerService(); + final isActive = + sleepTimer.isActive || + _audioSyncOffset != 0 || + _subtitleSyncOffset != 0; + return VideoControlButton( + icon: Icons.tune, + isActive: isActive, + onPressed: () async { + await VideoSettingsSheet.show( + context, + widget.player, + _audioSyncOffset, + _subtitleSyncOffset, + ); + // Sheet is now closed, reload immediately + if (mounted) { + await _loadSeekTimes(); + } + }, + ); + }, ), - if (_hasSubtitles(tracks)) - VideoControlButton( - icon: Icons.subtitles, - onPressed: () => SubtitleTrackSheet.show(context, widget.player), - ), - if (_chapters.isNotEmpty) - VideoControlButton( - icon: Icons.video_library, - onPressed: () => ChapterSheet.show( - context, - widget.player, - _chapters, - _chaptersLoaded, + if (_hasMultipleAudioTracks(tracks)) + VideoControlButton( + icon: Icons.audiotrack, + onPressed: () => AudioTrackSheet.show(context, widget.player), ), - ), - if (widget.availableVersions.length > 1) - VideoControlButton( - icon: Icons.video_file, - onPressed: () => VersionSheet.show( - context, - widget.availableVersions, - widget.selectedMediaIndex, - _switchMediaVersion, + if (_hasSubtitles(tracks)) + VideoControlButton( + icon: Icons.subtitles, + onPressed: () => + SubtitleTrackSheet.show(context, widget.player), + ), + if (_chapters.isNotEmpty) + VideoControlButton( + icon: Icons.video_library, + onPressed: () => ChapterSheet.show( + context, + widget.player, + _chapters, + _chaptersLoaded, + ), + ), + if (widget.availableVersions.length > 1) + VideoControlButton( + icon: Icons.video_file, + onPressed: () => VersionSheet.show( + context, + widget.availableVersions, + widget.selectedMediaIndex, + _switchMediaVersion, + ), + ), + // BoxFit mode cycle button + if (widget.onCycleBoxFitMode != null) + VideoControlButton( + icon: _getBoxFitIcon(widget.boxFitMode), + tooltip: _getBoxFitTooltip(widget.boxFitMode), + onPressed: widget.onCycleBoxFitMode, + ), + // Rotation lock toggle (mobile only) + if (PlatformDetector.isMobile(context)) + VideoControlButton( + icon: _isRotationLocked + ? Icons.screen_lock_rotation + : Icons.screen_rotation, + tooltip: _isRotationLocked + ? 'Unlock rotation' + : 'Lock rotation', + onPressed: _toggleRotationLock, + ), + // Fullscreen toggle (desktop only) + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) + VideoControlButton( + icon: _isFullscreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + onPressed: _toggleFullscreen, ), - ), - // BoxFit mode cycle button - if (widget.onCycleBoxFitMode != null) - VideoControlButton( - icon: _getBoxFitIcon(widget.boxFitMode), - tooltip: _getBoxFitTooltip(widget.boxFitMode), - onPressed: widget.onCycleBoxFitMode, - ), - // Rotation lock toggle (mobile only) - if (PlatformDetector.isMobile(context)) - VideoControlButton( - icon: _isRotationLocked ? Icons.screen_lock_rotation : Icons.screen_rotation, - tooltip: _isRotationLocked ? 'Unlock rotation' : 'Lock rotation', - onPressed: _toggleRotationLock, - ), - // Fullscreen toggle (desktop only) - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) - VideoControlButton( - icon: _isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, - onPressed: _toggleFullscreen, - ), ], ), ); @@ -521,6 +528,40 @@ class _PlexVideoControlsState extends State widget.player.seek(clampedPosition); } + /// Throttled seek for timeline slider - only sends seek events at most every 100ms + void _throttledSeek(Duration position) { + // Store the pending position + _pendingSeekPosition = position; + + // If timer is already active, just update the pending position + if (_seekThrottleTimer?.isActive ?? false) { + return; + } + + // Execute the seek immediately for the first call + widget.player.seek(position); + + // Start a timer to throttle subsequent seeks + _seekThrottleTimer = Timer(const Duration(milliseconds: 200), () { + // If there's a pending position that's different, execute it + if (_pendingSeekPosition != null && _pendingSeekPosition != position) { + widget.player.seek(_pendingSeekPosition!); + } + _pendingSeekPosition = null; + }); + } + + /// Finalizes the seek when user stops scrubbing the timeline + void _finalizeSeek(Duration position) { + // Cancel any pending throttled seek + _seekThrottleTimer?.cancel(); + _seekThrottleTimer = null; + + // Execute the final position immediately to ensure accuracy + widget.player.seek(position); + _pendingSeekPosition = null; + } + /// Get the replay icon based on the duration /// Returns numbered icons (replay_5, replay_10, replay_30) when available, /// otherwise returns generic replay icon @@ -878,7 +919,10 @@ class _PlexVideoControlsState extends State widget.metadata.index != null) Text( 'S${widget.metadata.parentIndex} · E${widget.metadata.index} · ${widget.metadata.title}', - style: const TextStyle(color: Colors.white70, fontSize: 14), + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1310,7 +1354,10 @@ class _PlexVideoControlsState extends State min: 0.0, max: duration.inMilliseconds.toDouble(), onChanged: (value) { - widget.player.seek(Duration(milliseconds: value.toInt())); + _throttledSeek(Duration(milliseconds: value.toInt())); + }, + onChangeEnd: (value) { + _finalizeSeek(Duration(milliseconds: value.toInt())); }, activeColor: Colors.white, inactiveColor: Colors.white.withValues(alpha: 0.3), @@ -1407,7 +1454,8 @@ class _PlexVideoControlsState extends State final currentPosition = widget.player.state.position; // Get state reference before async operations - final videoPlayerState = context.findAncestorStateOfType(); + final videoPlayerState = context + .findAncestorStateOfType(); // Save the preference final settingsService = await SettingsService.getInstance(); From 3e068b2beab44f6556d2a459706756174b84997e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 02:21:23 +0100 Subject: [PATCH 02/25] chore: update lockfile --- pubspec.lock | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index b1abf19c8..50c2afac0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -769,6 +769,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" rxdart: dependency: transitive description: From d744e783ac2da770cf24d4aab6fa91eefc72e30e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:24:30 +0100 Subject: [PATCH 03/25] feat: pgs & truehd support --- pubspec.lock | 64 ++++++++++++++++++++++++++++------------------------ pubspec.yaml | 7 +++++- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 50c2afac0..6ee9026ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -532,59 +532,65 @@ packages: dependency: "direct main" description: path: media_kit - ref: custom-mpv-config - resolved-ref: fd7bd79a9ba691306038c61791ea5754a5287d8a + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 url: "https://github.com/edde746/media-kit" source: git version: "1.2.1" media_kit_libs_android_video: dependency: transitive description: - name: media_kit_libs_android_video - sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c" - url: "https://pub.dev" - source: hosted - version: "1.3.8" + path: "libs/android/media_kit_libs_android_video" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git + version: "1.3.9" media_kit_libs_ios_video: dependency: transitive description: - name: media_kit_libs_ios_video - sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 - url: "https://pub.dev" - source: hosted + path: "libs/ios/media_kit_libs_ios_video" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git version: "1.1.4" media_kit_libs_linux: dependency: transitive description: - name: media_kit_libs_linux - sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" - url: "https://pub.dev" - source: hosted + path: "libs/linux/media_kit_libs_linux" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git version: "1.2.1" media_kit_libs_macos_video: dependency: transitive description: - name: media_kit_libs_macos_video - sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d - url: "https://pub.dev" - source: hosted - version: "1.1.4" + path: "libs/macos/media_kit_libs_macos_video" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git + version: "1.1.5" media_kit_libs_video: dependency: "direct main" description: - name: media_kit_libs_video - sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9" - url: "https://pub.dev" - source: hosted + path: "libs/universal/media_kit_libs_video" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git version: "1.0.7" media_kit_libs_windows_video: dependency: transitive description: - name: media_kit_libs_windows_video - sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab - url: "https://pub.dev" - source: hosted - version: "1.0.11" + path: "libs/windows/media_kit_libs_windows_video" + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + resolved-ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + url: "https://github.com/edde746/media-kit" + source: git + version: "1.0.12" media_kit_video: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c2d7acd8d..09dde9e58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,8 +33,13 @@ dependency_overrides: media_kit: git: url: https://github.com/edde746/media-kit - ref: custom-mpv-config + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 path: media_kit + media_kit_libs_video: + git: + url: https://github.com/edde746/media-kit + ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 + path: libs/universal/media_kit_libs_video dev_dependencies: flutter_test: From 2eb33eb234326b93ab1b4536352cdcf543099d85 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:44:50 +0100 Subject: [PATCH 04/25] fix: android icons --- .../drawable-hdpi/ic_stat_notification.png | Bin 952 -> 1122 bytes .../drawable-mdpi/ic_stat_notification.png | Bin 688 -> 820 bytes .../drawable-xhdpi/ic_stat_notification.png | Bin 1228 -> 1445 bytes .../drawable-xxhdpi/ic_stat_notification.png | Bin 1794 -> 2181 bytes .../drawable-xxxhdpi/ic_stat_notification.png | Bin 2417 -> 2843 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 4178 -> 4708 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 2728 -> 3052 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 5606 -> 6581 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 8473 -> 10735 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 11596 -> 15940 bytes scripts/generate_android_icons.sh | 131 ++++++++++++++++++ 12 files changed, 132 insertions(+), 1 deletion(-) create mode 100755 scripts/generate_android_icons.sh diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-hdpi/ic_stat_notification.png index fec90a263360a675864a59b0555c0eb2ac9b069e..fbfeb059386405d5cf5e5e9f5b8b1d563d2469a8 100644 GIT binary patch delta 979 zcmdnN{)l5jRz16bmer!0@hcb@n4LUb977@wpPk{`5t=G-yngaD=O2P*yF655R6Sn^ zIA_h+QlPxnq}=HOyQ9Fw9T};!1Gv(DDe*46;JZ>|hUtq1Q#S{w#6(P+Hh;nbXEG>^=|{XUro5Ra^C_! zuin+Sr?&k(`+rsVU!Pcmdk(T;*IY}Rt+Mwt{4CEe@`w%<)X~nd|*n7A&e`wPPz|{wvJLk-%cXvxUi?sr+C5f&TMh3U7H(wf71;Kd2FS6roTIrEq11-5JN z_$Bx?pwn>qmGG-CD;s`YQQf#U^6uJu0q>7|xwLX!*Wcb()>-AR+G?{_TLsTuJ+EGy z(PoXDiOSb5hwWFtUk$&qIkoO@=~v&G!7}Yh(hpuU@@3e$Ltu|LHqVrhYATF8C7G@bj?ClBrCu!siDs zU-f=kM2<$&+0M^mtE_pJnTs5MA3Xi&zsgswLJHfjmVK2hmUyh$e1D3u?bY{JT{|Dg zNR+R!+}QlN{%S1SWS>{*UsDfXd|vcw&#ndgQVsT(oY=o)o=@-oRr|!t*Df>ty8n`5 zft_0l{|Al_A{!J0TUniW@40zgNi~&wdVC|tCAr*%i`1?&E?UujKrDg1K==KUDu%l( z`;?|^*dK1abDG291By-)@1NA@{u9Ocm#K#34P#z!>cWjov8?;*8MZM_&;EGYjXQ+l zo8UjM`4Ug1W;(EL;x6Z$xnI=6q0eAl6z6oC84piAzqV~taO ztH0t5;^J_AmK&Zg8Ta~cg2s#EZ>QdiEy!M}=xe@yBLf42YKdz^NlIc#s#S7PYGO$$ zgOP!esji`+u7P=op_!ElpOKZRg|>l#m4U&p1^j-K-I--YATkJv?!VhVO|D>8P(YWs U^J1DV0|Nttr>mdKI;Vst0A8lZr~m)} delta 810 zcmaFFv4ed=Ry`Z99>*euIav%0Ofx)P977@wpPg~qJ2X(F-QLjT^acmzDGnM2t38|9 zJY6nmXmLngT{2VPhP(i);0 z%5SZ{+6C3iub1Cic}jUgjCC+K2<38E*w*;?Kx)D+kp?GTCiTYb13sDztyQct%vFs&k3^(2LhLW_-I=F< zLEr<&51tU_*9~F|Y}_}r*B?lHP`Jk3hp}csUZ-Z<>8pJUs-Caf{p#2bFWIa5TYYuA zE!GBYUp@Ou*NM+tU!68-OuuUV%Aseif9!m&70O9>R@wP(s1NO5F}3l$)7I#}b5$O6 z>{9*^E%SJ>)#7g~#{cCnI2cIJZA_jsZ?&)BkE^c&zqVDm-d!K&`X*OW=W)ladIz?? z!dU_Qzup_E^|^gb6T81>R{s`xrghBU8v_?`MKHf%`Nkj`S(9xedck;sy2n+22i^-j zZ`i&uP5u05;u43G)7~`iI(?<~^eLU`N{>~&SzeW|TguCvA9jBA_bYK*Lt{lYZeL{| z#8R;^FPJ}t!FbIzb*56b#;mK+hWdS0(>v=GLxK&WFK+p_NbXa9C)wNo z^Vu-kvE97Rlf!UI=+C9Rb*k6vbTkY+uJJ}KY$(c0{LE|eUd=ww_Ca{vP7m8kHMrUljlHFC)ic z-GzS7Z%>(N^xwUL2=fx*+&&t;ucLK6Vojb^U^ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-mdpi/ic_stat_notification.png index 1c27a497ffad9a8fa8a8ac3126ce32850f956ba2..5c7c824083faca155f367a8aaaac95c59d6c931a 100644 GIT binary patch delta 650 zcmdnMx`k~*Rz16bmer!0@hcb@m_$5X978;gubt}M(VHl7ygvEZF$2A&ZbhDfho>yf zaS_`TpkvY-gJnp{?)dE zXO>=A>EdUbFFQ-DfippD14jn4k>ka{1srSG*&2KoPCrtfP@+&1AGbEA4G0&@-Y20*rTvXimgrQvDcG^)t+MYzN_4? z2v0N1l6-&F_TrYk)3&|qsIrN;CeNbzcxsiy+~C{`n?j|Q{qo+sVjbV&*jGkhm;5p< zId*09t1h?ZF#h?;4BQ81NgS|0uvTJ=&tk`HHrKC>yUtpzPTIv6x8nDeO0$SESxxWY zW9DN1VyB==(!+t?$YqIlny_J*sm}EuFLk!KVOpL5dEwl{`tPBi(E#UW?Y>!2v g`|tKolM9#>6wr0tc`;3wfq{X+)78&qol`;+02cHiG5`Po delta 566 zcmdnOwt;m*Ry`Z99>*euIav%0j1xRv978;gZ@uQ*(Hkgp{NwzOL5q5B&N$RGbyCEw z$gUu_SxYwsnJJ}aI&VDaror7CU?#SCZf~Sipx4G(qK{+mojUtrSNFwvS6^Y&d0;mvC8u<^Gpt^`S1?>kON$wiNI# z_T{vyIcm8k*XzQ=V#TP-Qx+Vyo^$9_9n0s622Yzq>!lWnTXc#37m7LF=C%Cnj$q?O zkut|N*Q z5+Zi9$(7%HVpuD3>$}qFHr*xf1-?DXTzP-Z{w0~RQMX(x-O9Z-{@?2rE1c_- z+hdcDe_fKkKsQQniPT001_lOCS3j3^P6o(VzO?t&qHinMuO5B1 zq-AHS{7rtj!0)qq?!G$xO7&@&m3ZUJS9ZUTi+wfRy6(=Ybm1R1t)C=I+&(W~dG)DE zz*3=wq7uG|8I04I-ZNQq$5?B02)|%m)iq@^cLo0s5sMf-CO(&dEp@^l%r=O zIfrRqM@P;&7bZy=7CS~UhTZkr6CK#<89p~WJ#f)Wh2a8o4f`I}8s?HvsYV_@hSd$_ z4e=lq)s6iPYzg(MLMI-iC-7~M%2@5f#IU@v^gvDm^TT~SR*a2m3CbU&72is*#&G*E zu48ZGpWvarfoBH)8;`wO(_a!Vwv5l zyz>jMGYuehzU0HYNbNW?f&8Ms910=9) zp10PQ{-|BaePMSe@55hDwZOIamgx25Y`GGVf6BTbrdIXfG<~Cr<%RQZ2CXza()w&l zSBXSSZZ_ko?W_BjD_&T;@4?K2$&;+Y|GNIY{y<7xGRdWW&I#LB=juc(*VyHL@Z*=h zQy_K5e5rJ<@eE(JUFU+nKNq-_)gk#&p?I-k)3(&Jwz&q4k{fpl^W>hc$q0-8niD?f zdW&7~`*3@cxx1s+#feQWUso0GlDE<7_=MY6jz7DyGGA(6g^=H>(^sN>_SLV{D^$E8 z@Vn@5Z|AE^GSy#J+^?5@bh4wS$V>3VtgGf%qI*TJ%-2)V=Sk@5SHTmt|Zu zwKt_DaM!X1$QJM#NbZ=l`SuLm142&ow`#ss``>3gzI_d)b6KqSi&w0RImMiwvN)J`YFPb^ zoD9Qj%T9`U+isG#S@Q1W!XNkB-YMPQQKUV$pP_BpWv{Oj77E>7Ubp66OkV2bWA*)K zZJLZ#Ra_=(vB-*;hZve!nHX7_T4);>SQ!}nTEOo&IU0*Z_uuWGCik!?D4^@O^J1DV Q0|Nttr>mdKI;Vst0J=6#NdN!< delta 1110 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 delta 1707 zcmZn_Y~q`cRnNw&$FWFZP8I_L>myGW$B>F!Z|7upgocV7um7Gk)3ayRtcMYgwGZj2 zHVGZ&nbKiWaHd1RUNp_|3VWc5*F}*|4#Tr@VF_P09NDmvW2%zJo5X}^ZBcx_MlXMO z)tH=nv7i67+)loE)$c0L+I_zz|89EXw_DZ!)83m_uig6ntmmTo=1&JI64(l)4J1x5 zvN!BK;P#;Qfywm7B3XvJjC*){I5Ns5Dg-N9wb(;>UKA#H|Rmn3`_OR(L*TtDMk)uh5HlS_nNOf0g$-H_g>yG~su%rf?o!jwhZ5*BRR zIj>^ps~m@UX3WKnZ`U!hXue-@pYs6!f#nB0t_ygbyvn6@t@6ym^T(ZL`e<@D*lcB# zXE@LNJC^@g@HB=>EABnaIt;U7`PI4`Z00guW6)=4kKt81-C#49C5*|tvEHhF_h&DL zCoegsB}F=_FN@IPoUmL>r+9|W2S$aC;P7?EYCB*Faary2gPx%x*FIjrNR^jE9%AdfW#U#hJ zZQ75y2U+clnJ-9dtG#2lTV683l2_Z}ThGJzTaDq|wY)6zn6sR>QFGm0^-VMz zm?WBGHQ)8mUHdKBZtazgNn9V)rfuOio)^KndCQlnOKu5iE4~%UyQvu--k@@ORdI@) z2Gjg`0YSp`5^_ZoY8GfQpX=?2zku_XDIK2%}9CLg%mZqWaH)4Gs#`$L!)xCQNT z2tCSRhPMkD-;KlIdi&Et@#?(iePGXFyuMEAXF{FO73=v~U{pC2< z+kwgM@S~+FdWK7s!W;h8UwOS)H$uR2B3A=5+lu-*p9*7g564;gFKD`VrkU~9+(my~ zg8B8P9A7ob^L2QF);Be~iSDZFrcAyfzI(<6W5(*xpI50pGw$}?-jt{)r^Vpp;7 z#rzSC?N4+}oYC?l^|NT~yu9O&7DO}Oik%k!sqA5TL}n`6vlRBk=#LD~R!eP|v2xYH zieLP-HxBNu+*YqHz`($uTH+c}l9E`GYL#4+npl#`U}Ruqs%vPdYhV^)Xl!L-W@TWZ zZD3$!V8HRF`4WnT-29Zxv`X9>DmPl^F)%P_z-=hW%uOvWNz5$(>zSOu!K#36iQW>a RjSLJ744$rjF6*2UngC#X^(+7Y diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_notification.png index ad4d0178c3ea66fb502492e258e7681b847f6209..f0ecd616896b6f9548b91db53a3f0c2ac6035c3b 100644 GIT binary patch delta 2688 zcmew;G+S&!Rz16bmer!0@hcb@IDdP(IEGZ*dONpzLujb%ar^1Z8>A#vrIhU2w>Ua$ zx5*{&)fZebNuG>4>NBv7r(d7UcYx=_4=*#-@bp3e!ay*NlEv> z%LA+jEDxMK;IM%C2Fo5UA9gwRFjw6zH#0jm4g@^#dZ3iRA+}iN?SY^KqYnxjEZ=jl zDPP3#!7{<-!%VGHZyBCVv->Wx!TLS-+Hft-53C7X1xaTk<^$c~hKKzVhRZC3V{~6rp2pRh5~aF{&H{s3kloY1X(z4bh5wbtT>Rz z(WT;WAj&3nFY}N5dsA0VRpJu(AXQ-eA^gGLJ=((4mADz^>776J_^?h+u}Ie#IPphBh*wA3!5Z(B@v9~f@ZNW?yhH~fs$L?M#dCf;#~8z> z|C=we{=xDx<`1rW{=C{yFY$ECrOfOz{Wr?y9;snU^Rn91w98rb@1BzBIl94atCuXD zq~>829OP}D`gKxC>k?niyI=dgtoC{Tv{i}KUAec-=(^{xyLC^)Y#wo3P<*-ju85Vx zrPN7rt2cexKiy=BQ|tNFo9_I*shPQFiDT+1JGV>Hrc-1$xLkTViO<)&!OXM%d3((} z7Ek^u|J3+CtIiExcWmlr{vpT zjTfuxdMx<#S|wI%WxBbjy~`yD&vs$bs{@K%i#Uud{bEi>AQiRH|il6yOvKa}lh zd!zVgQ>j^jZb6L`^Ao=Z+iys`*2}mgUoX+Qe2sFW^2W6rdDBDBAN}~iRP>xVSM()i zp;h(aax8l4Pjq)pi@Xq*AX~=U&-6aL*xQ`G)tjy!DAAn1Y=?K@ z;+W&2H-fDily)riXS)6%GkKCsU-mFQB*({RH@?q(wshS6b1upRY;a)!7#`o*%p6G_)4L8dT<@7f_(R*--Su0e5 zktwQC{KM=Yn=PhEPy2g~Te@mK(>dmSTr!h`_r|l>?f%cdS!4k(zlWpE0{(})*SVz1 zmZ?sEw3R7(L#PddKabxru@i*{6g6XlP4{@tY_#)b+*Z$gx?tY3B$-4mrKG=Jye2Xh z%oe>*cCiHR-jEp()?nh)xO$`IR|CFNIV(SW|JyLR;QI&NJ(?l9lbfq@YB*+kssCij zIrQ#PO3m}*n#l$QiXWWcEHD$ATsR?9ev(jv*}O|<6=&)^5&rn+Ptv5V3qB`_f3V$S zf6VTR)&V`4dAl2*)CmYt=?(vj&8Kdk^OChjVt?(o zBl}K>ExO6NxG>B~NZP%0DR26bz5VIlnQ!?nw`j0i{^Qj+qQX1X=kp= z`MPf6mvu|7tJw0(FaOGIgF0-o~W^XQf&KV zMd+Lz2c$e7tH@sSZRMZ(h5IMxnrUU8{oYo(OWr7}==!X9Snj_j;Z3&L#4mzXHrt=c#0tGWG5cxTN}O*=x4|}{gnqV`dbFgWxH)x z)o0yo{OqPKq@E`Dne#-K>7;%7EB#H&Lq(5kPLk`6++OB+{zP)z{T-zmO|o~p?wjT* z?7SQJ>ygWY*B$AVI>!q;*;f?Szfs~4*ub%Wg00h^yMa*!AO0P`QM~Hfvl|`pEAo!M zOFJv#$xvXEn72|ejp@D1pF0e1Hq}Z0P}_EB^+M5l{p`6NxvfWTEze+L+49))fXtq` zH;ZIG z{C~wfZD&67cE;~PYO@tJr5qcU-0TuQmv?O8xsa8|j}}&LWbfC0T=>mZZFZNu$N`-r zr-PP1j3`Sp*%vP8&rDV`SQlJ6pfbt7a!ps7@B-%V?}ZyAEuyAspH(?v zv`6uH{E9hRlO@k89ALA0+r0hLx-J8dO57i~>gy&}%@Fza@nn}0 z*9Nv|_HrRxh2ZTXo(*q*u`eq^VuDne+Dvg3MXj$-8|j3>-JR}?L#aPo@Go|p6>e> zDX4ZpXU{6@v#RrFzxA>tLpQF*cuXsJ&cVAn3moUSfALWm2Wgi9H$lr0g%ZsTY zeBT|8ntfeGw?Cem_EF@EwcKW%lrZrT^?^rQX|o&)oks`8#c#k>D*|1{Z*euIav%09D$xLjv*Dd-pD=rI6Iwu&l8mi4o=uivc*eBo`So4URcahK$ zfzpqS3tf5@`tMG?TW>e#WsyXAdeyG{^Ka+PjK5mb__o@AetmuU{oBvy6CUfTfEZ2UU9W$*IPrPB&P?TpdXE1KyyMIhYwV$bFPxILs zdCJEXKiF^!TU+>DPZUPD^tOl$*Cd;%QkoIht5Z;*GxN{$Gr;{?5z$cv# z#t#hdw}mX4$jTAP62rOY#qA0AGT0cM>ctqRGoG%I4)U7Q`h8Ck&xR>Itp{emCf5btJ-w~{=3@VI)pHKqHeG6WIf+-Sa&Bj@__+ejhKBSFC12GW z!W-((+5IcyFc7nlI(_2wnLT^B-W~|i;lAVfl0~{ov7& zpl78z+%r$+r33!VxI?WwdxNn(caX~law&N3 zpSmUbJMvQHrtj_Us%xig@wrsvX`JH3+i~OCh9v3C;^G_W zbG|qDB_wzmPYQa*^n5~*$)=W#H;SH~u=;wP?as`v88564?7Sgkt;?2VdUkqZ>#MIx zrW^dPu`+OfI?K53z&*tnBgPqR`x742zpNBcSi@R#j7*^ljHTB8oueKX%yYv>=y_bx9@V=1s1yh}~Ri<=x zQl(|?yt%>rw;pkP&?+$d5OnI;+=jc6_b#yoKg&%BS*x97p46?g)oOn{53@J}yKwSM zeYWh4+@~{7d5YFCmALXA%MJZ%+IVSeIKLQdYW6i+`P7G-+!~(ex9GrUtfH?0R7kerCGKp6PXL z9>1m^t~<5UH^|YHtH4L}`5c|hP^QIl(@mnTx=QF}uFGLm*PHLL%cE@hx2xxQ(%=1? z<2y-D^S$;e2KyI>>o2@j{d;m^V`zYTlEK~UVob5S+sV^ z-qUE(nLzI7%?_d1vvsBbN-k&*tS(2&A>Qzw$Ehx<7D zB)=(JY!=4en^atTvA5TGdx9y))@QwZN0WTSlOIRK%vrkpxo<9SyZj_bXI=5SBshi4IrM+p{@NDAZ3mf>o!jD*OST+51r~I|p)?D^tg93fs=i#a^luf?~;$x81eT?)tjt(x!-XIlA^+^b^h_McNC2jt5#uP zU|>)!ag8WRNu1owB`jhVVrXn-VrFGvp>1GbWnjSZruow3Ra~+d5|taR^CsWsQcytG Tp|?b8Bgj-wS3j3^P6 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png index 6b97c20b2af855c5abb1009932a6c8711ff93a59..6e09b9b804760bfd48e80cef965fad8d458888e8 100644 GIT binary patch delta 4600 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 delta 4083 zcmaE&a!FxARy`Z99>*euIav%0{6?NGjv*Dd-p;Mu68l*4SpD;7ixhhm+c+*R5_MkT zp&4cBI`z^~r6}jMS3Lu}u1=GRx~df7d)j8oj3DmJs~4n#mWuMOv~)T$!O?Z%YqMsR zyrTR6KHj+;V_t5N?tlN@?>+mso>Q7O=kL4P`M=-hSKRx)c7~*2{Y|DDvNyzTuu1Un zFoiKkH6A^{^FZ|h>jTDwsw>@=PnQKP7iecV&ak*KQE=UNEd#zCQa}7(%r5`ryw*vE z>yMO$NQ7nMYdyw1=3@-&TkAOdDt-w)@;Y$xK<0r}nlbx%CAjDCp5gjyFsI;)+7ZPA zvIoSCJ02^moQP*CcCr^yudc5Wdi1=3z45D2U%Fy}WdWms`J2Uso0eEB=5gEbtdTWX z$9h2YKxGE&yD4+lImz(YFt3rf+Q1jpaJr$pp>kn!;ibi!1!gz8H^e%zU*&zjK=hkw z#?iM^dY1Y9ue13nqL{H{pGfZ4ISgxrtQ6EES1CV=dT}Lr?FGiGCwv~nJSaa8XbU}Ax3IY|YdPl)%@P6453UaaAJ`?hyhxa{ zFO$iPQ`V)chPOg$hq!FCmF3dTq_xu)2h=lub5?gNXPD0`J7KeusB!eM?m=^dF@$=~@gXo}S9B^}%w^V5_a<9e$VgAcbxSQ}5UyuLPF^MTWv`O_PX3zz#Z z+99yG{N4-szz%`*!Kkz5^_Gs>HbE1t+I7eK3CVt1#<` z`;jM-tUkxqrk6fcPTIAF`EldrkGCf5edKhe&Fn$hmbahNUL<_#;5LlDQ+175?}&kp zi@Hgk?dt=r64O?F&EffQ|H8qRBds&|pDo%ofj8T>Sn*kXciYx^I|V+U%$ahhlzD?m zi9zH8r!(fp83M0oa&MIIagDZH3R2MRnz=Sz??_UR)$5tF<+?o6Uq@=L+WE61f=`WM z`GQ||tw-FC6d7>+5Z^Q}SJ-NX#*dB&yH(dVU)BgX>?<#L+Fdv+*+z7S2-o)A~;)MzsCm{$csSVV1aOa0fs0edpEp_FUrm zBVoav?{-i8zst4F?w5XiGHI!|;QygnAbYe>IKOokL$18l1SSK)xyk=+PwM+db(~-5 zcF!~CaLQS)jQF=pE;Nd?RA1Eo{ZnP1j;CTDYagqaYQ^TWD>9!xJJ8T~-d)46R!mEI z&X<=5GPWe0`TXy(%a)EAeal3Y8}ldD-hcb8&*B_QT<2$3PmV�-062=CddV)(amG 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 delta 2573 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 z�Rvihdvn-`P4xMfAmLJWZG7-f z!ZfY0KTtjSipsGS7b}fs?KpDMV&)M^;l+k0XI#Aa&Mm~jO!VMX7v;r5*Cj+<1OpgZ z1DTX|>+B6XE5Bb$TJ*)P(C7Dx->rRLJFiYae$}hpSHG8^x4a)!Kl%J~&k1WAVjG+r zn;Q=vnCDQxK>dPf2HQ0Td4|u8kq1m3q$UVnn6%_zTOg+?C+8ae0+9lv0+SDx3&LBj zuXlS?B~~F)!51;{g?Xw(s=)RGzZjM?K4)rYylqu+e(Qmd1Q`R)9|q6fpD;M3rq`Iu zI)@{Mtwu8UZ_MSUuuahoC+iQi&Df`WSYx&Y%Qo%;)&j;0){WT_&0(9^9;hlzKaeiv z=D9=chU0WaI0H zzm=3Oao99IO7~xUMdxrwYZ2=<<4>1lU;bv&N{}qjFR(T^yxq{vOfx)T`ogz~Yu|Gh z9FQrolWzYc%X?VzDaRUntHUzyZdbH#sb|z*^!euefPIS-LwuGS#eEiiz;M6KdYM>m z`Fn@eH!r3nmzvoKhbF`Zp7b=o-*)`SwgWQ{uuS=6dSJVw+pf-Ct}+aNCcfJFF>P_^ zTe%R1EQyI7yP9O#-aNVef<=<$PQ#ww<;>B)%}#iT2eOt4yzH(H^-3>!w?}yAm;NK? z(%m}iT|{p<+-Iv^qF%sfH9wa9&D+}w$~BAT9ZXx88@zQgdz`!H5#ffX`*@8GsjNC+ z$MWl!nU%t<#^nvO10#JV=QsCk=+N5W_UPsMiDh9-{7lY)P8+8lm2fC&eA*y&-;MnY zlgJ7=UY#$wJrC!vVA`x+eEi7;Ei;}S@)1vOD;$2M9~@7z23(dx@)DARhl_8tN~uQ*`q( zY-~NPl%Qj#t9G@1#(_iWY+OP2yM4PoWz{y6T66grJ#7J_*mlPJpJr+-J_&9zdm48M_PZ<3S|uhtqh3%v zOstvnOvn8G3Cv6QDmt~icN;ZIdVVZ_a!KA%)VJHPQPR_xnRTW5q6MYP3>ziwGMrw9 z@g5LfVO^-UQmt|8!XG};R_t5E{@vkv!E!~c&WhP|U+-F>u!Z}6eLb*x$?t!+R3b$q zve}nf1_l?aT((P5OlD1sZDpfq?Wanyr1eL#k!4wmu0{B&YIgcw=RjVO%rEwXNs+@ zx0GCBp_?Y7snhK_`M|V=H%@XKn(co_+(Pz;_ydP0Q%&w%?Aqk;s==NyyH+|xD^nms zyKCwl9v{Uwwm;=EOm_=ddhl#tv$Qtq4EK@+I%*}2d+Xb~ezr}Xmo{VTf$0tZ8T$19 z+$}gQWRmcqETJzyd7H}$rqc^UUteF*px^t2RgX!Jwd_l-kK4@Ftn+FDFSg7OVBN;G zHmdZ6=hQSSMs>#P4DG+n)D~}9^ub5gVCI2SNlwByVzy-6_1<=?=`Hskt{-AvXN~S` zX0vp76&Pc}p&sE;&s5E~L+Hos>-j2nzZmRU^%$#unW-HXS$>oyP)xY#K#|77&*B-Z z-ILEbOg}V#ZombL_M}W*7yp%AUf~beGfZELe@;K@DR*>9+G3wsYa44D&I`Ng81yeY z%H!I}p}$H>O`d!bIxK8@uY#Fjo=<5b^y;PVCF&sJO+ z#}#+@X{41g7rkEIo0?I&gZ*ip*&_BZ`3DhOoaEo!KEc#`ApXM79IlV{4))Fdja$X^ z{z%^4#7@)-94;-WKBmPd6U+Tvn4J_$i3@@ZI1I`X7WJxKG(P)wjv?MdK&-MaNaz z7iiA>TN{5Q?MA?c^?Y~xH!g|&DRiTt#5h#wwG=}PV~zL=(c{xMvleOm{KED?@J^2O z?ggQ5vP$|sp1pmbG-Gx6xvyWD-shCy;`nH{!ka`+1K&p zDC@6#`#Sby?mu`oc$eheGxv4>yj(5lqiiKVpSNexpLX|+?>%39asGIHqelA`&3!D} zw4dfauUf2f>vmvdNB%bF)h|4rY$!Omdc~WkK~u7)tK=)WA8XBMUDqPFM4f#zo8>Gw z#`K2);`_FBaMw5HGZ-uHNz>nwYT0aXx%VE^I;TE?aJ|2^4WBP&ZF29~w!b~>Y@kbV zN>1nIi(L10-F81t&@6hzMHt-iXYwU9L>*5dbI4e{C8(ZrrYhs@q*`n%*(yt z^^O0<8*_;pk}v$K=cQgexq&gFzQ@Ism-Y00rFolv-#bvR#9w%8TifhMst>APD29mb zV`baLnYM7&yq_+MoV{Oa^^i(Kpf>rJ!P>pAb7 z_PTTX-9yhCvM+|2oz=d+d?)Ww4e8UBg&a($t^5_uKZM+GJ96URf&9kFwbDY&s~bPp z2mDJ-S@S#aPmO49<9^p|^WH4@{rt(Huc?7C3+5i|*EBwSjAa^6^M$a-=`B;aLOE?G zF3)9oS?DnJ^u+_5PxoBd)^YMu(=yvQ??b7=eUcqXb?KAt85f19E!3IqVQcS`P&cDF z=4ZL0yGzuIzAo2#wwi<64LLW9{7_pu^>s~8u3-IN0|wFPdwnr)7fvmynI#p%cBP@} zX!z_M4hKK$yVp)!oA58Pqv`G9j_Y&d^>!;=|IK>f-oD`o5R^f0R$9SMC&Z=sV-ad z1dChe+q^YTl6ukCVz}bN_hpC9yT7Q}u$>9LNv|z$v-JMVUVEW;x9lmm`;w`j-#5*BrCk!O1G+0xZ%cXNNi;~%*meUtkEO(Jn{kil4>(Mp)`iB3V)L%^|oNIV zs#kQr9M@QX{siB7hS!VsC_K@9#B?R6M8~jyMtx_a)Dw=KTwe`+ycKTWu;~u8Kjm?@ z+ev4~nS~pTo-gS9m?2ck;C>a)B*7?(S~-Ev;_jm+_&47b9CZclVxw+5Zrbgbx0!JL?L-OeAB?D~Jb z4z=pf^1gn9UG&u3-YA>zt}FfKPFP-K8s#W{AueBZrEPepj??NdK~K*4C$IF-mfC40 zS0Op|REhh_l;w-qR^R?@c1f^u%7uVyuUP+uZ#aIj_gR+O)#KOeTa6y|JJ`PKcRKXy z;Iq@Fw=PH)uy$qIHp_^ve;k){x54|us=3P!e33F)^H|JmuH$A=sf}N%L(a@Bo98yu z>iyK;Y<8?>yq3l>a{qQOIP*O?;MXOFkaK~0-T?xYOdnM8E^o@ZcnzVy~`=2(3w4Pl7X{LLi3 z*z8=P`W>@{_at~VF5b8*a@u)<^?#QwdwDxGGoFPn>D!jE;D^ek>)rK=v)YUW>)F5W z);IsF6A~H2d3%BCAG?x1Z>8CLwO6t47B;n)eDI=n`NCuK%mw|l-+q1JSR{AxDC;Xl z3C(#X8aJkk)?H7`wsu#PtqSQAFH%BIadPyojpc@2jB!o>7kr6+ zvGD8rDf44uWf*3#MFpOFy)z^I5l?*HuYDQy^W#o2eCb{3dV587Kxaoj--5m2zt3tE zFBXz&G(7RQmQ^T!QZCoEMY0EdSA@o>mbmgSx=<kh1=WxT-Tp9tqpv8(e;ImaJu=5vBaMmtrux?`O0v~G_y^3-*L`*HrFci z#oJ1~1H(?mUyyw7d+G$=`-Z453V-qw)Q`W9Mu4JIT%5DZ~9+x|gv z_u|>lL$)+6ofvxH{e>`<`gOS;=}!!9F}G^|x^(a0cGkJZ(-wUSF8GNp9_pTu*6%rUP9fw;i5e9ars#Tt^S9WhgqiS$S*Ly8KC7a`uXHGjhq72f7*<*F8tgp zXIb#bDtTHfr%q+mg@eDhpO`(}?3;UH$K{XKT$=r5tY0-$Hl6w|zFE^J;a`RL((vA; zpF~!xyx^^2f2#5B_04IlEA()`hW3J7q&f1Gm$g9wx-GGWtZETxURO?ke~xm zn%V)suI6emtN!?XkwuJ6d!qGEvnw|?E$(`#@}j40qj>eZ?_V>#g;#m6*8lU;%0W(A zZNr9$rgQ1Z6D}z6Ru{XNUnzTddqrb!zWTA%FAf?{6fr9>ZEQW+uHpW{F2r%~j34`d z|I@f$|FL9>>*L)?4{J;8WEuCoy*X`@VPorAwvazRRBlO~bx10n_~~-zH^ZIr&QDdD zKicmserz;px@YB?3oO&*t}WJSow-6MP3@FnOrzG3?In6IyNquCUNGnA!4+SgaXG~Pd zeeoxvJ&WP`4>OHCp9Hs{J-u38%^J#{T*5O1%})pZKM@^xx@pxRmb4>B*&c9ik*(=E zaLQxSA(pfqYVAUW()AY>^q8ExQP#$Jrr_l66_c;cWr=f}Bm(NfufC9VwL$Iq605x@ zRn-g@3(dOta~0#)ZD(AnCVTrNxb582XVWa~ktpdIG~+DyG5 z%Nw>;cP2BPRy(@UqqP3To~g4kRU!|wOtVc>IRBtFWsP^9lwo6PyN{3ZNh!4rUy@T* z7?YX2^`)mz^l__faZ3P=1La&%{Vq4cl)-WHf2_ibY_1#N>_|O-fwRbL7(OpdQY89T_tQ|4VkgM^1Sd zHE^D>x4d$GuUv%$ud%sN6XzNIFta#U~t5Q^WE{aOd54w@691sfkR* z{@U~BxFw~kg!+UuK5dk$zwgHGQz&(EZ-iv0V@abahn$x>F3rl3>Z})>xq@kS zmyz%)$Aro+4w77V`hV?_c2ilz^``bpN$P_9!^vrkR_VQrV!zCedFThSUgNQ0n!{W2 zJ-3Eu&62%6DOP3YjI@@h&F`<_|LSV~@Z!GSau4HKZbjT$x3?P!9p-%cL`lzC@73W( zy?y!%oIV}MUNP~xOO90M!x=?k1`V?H4Id8}?$ckubdUYm!p+xg?jMu5xg@5MaZb}5 zhP}1%E-u$D#kqY_GV49kG0{(SPnnTdo^QAE0@0%z&oTd#e9AX#M^D7c6jtUJ{o5kc zfA5>XH>34jWSCo$iq}#m=BH|X41Aw!4QD49YANqzp7Hp$#NuBCiUtkJ0dHP7+!qf? zkge~!es%HM1-{CLdmeZtC~W`h>yfih!%tbVOGeVH_sEAX??%Qq>@n;$!Yh1#L_K3t z4(vI%Y@&qYgQx_C+syV_f4=xXkTRR+_Nj_#e!7IJaQgv=cmEeRo^E*DC}&u6%qqk! zh~K8Utx4J~;E3u5rQ1x$8J{!9JFSjhs^gw}@t8t=<4w!c9Mg8NhNVb95O|>SAp1a^ zgS^-2)*RQ!ckda30~)HeuH|3wVD#i%@apMJ`aM@Z{C~Ey<8gRgG&chSgKCLuL`h0wNvc(HQEFmIDua=Mk*Thsp{{{> zh@qL4iIJ79sfD(Ift7*5uLb;mC^~ZUQ!>*kF?GN+bpPG{Y4S2r1qGN*euIav%0BF8*k978H@y`5V*CG_glWA&j5u1d`ZAJ`lf6Kq_e zGHc7FTp5Kdp~Z%L7H-M2&F;D-TS;XUdTm$p>y;8tz9J#~c!q~b2iLi#-Y(%Ng)9e$ z=}Ims0vZeA{{48B8F=kOTlG7;@B98nufCR~xo7+Rwcqa+pSP?3eS7!10F4O>cN-KB z7$j_;X8)c2*VaEJ%+t32(P2$v>^*S$KuCh)q=OcHrlp)5Yt#*xELeW9356`(!{Ed4 zj%`_Vyv*^H)3R8$aTX|jU{2^}*yzi?jwz4z+Kd?4<0~h5v0UTdAz&ey;h}i!=K<9N zr4GTX!jBiOS|ZbUmH9Z+^!mWN$q7r(u>WJYBTRZftM3-N3ts`E5&jV{v2WwMGlS z=vUT1>+h@86*ATc?%eV;oHau_W zUE92R(#Zoib>|-~^vU?$af9Jc=bLWsg!(P^yJc^jw31kEmZ`mA^M!e8`d1j;8P+qD zGgwEkKe-~dR$g#BOB}1&7CtS7?0`qlY!doP4gCIP?rn;7*>{xnREJ#qxeNdJuLfKB z8FXpiNIPKj$p57JS*{1-2hKNsU)x+?_{qJH)q3JBVeyQkcG7oa>e#+)WK%bp{5pg;(E5B`hs9@cXt>TZGGc(UZ2%hVL1tGlw&+UejE-$=i0r^il!AOY*ix z=h*xdXN&6kvFtctm7e|J`hjeZjJs=?i#EC18l14|;MpK6bc=h%caOQH4t{1Ujvt#l zr#`f8L7R;}Q+qV~<0JmIKUY}$%wD+SOP1D#^`!=Wd#;FbwQl`!Z&8$*V)vQL*BUJf z1H(Obe9q{7EGC(5`)YMP=W>QKu(UAW~d`*{+UP0s0(=jz&KFd;1BOHg28yJ6zR-ViJBH(=|G_<3JYk_7f5hnO5uG zQJQR|x8c?iT_eqhOsjLnn8F{3%}m&FK&GJE#~>!bFZpw|hPjY@VTcoH5YzrCO9wP-ypa_cSG{&^9~8culXOs!Yzi;k@3;Vcp5^EE)=4^doU8wkBS*gKX z!PN(5HQWy5-stu^;noFa3$7oA21l%BNG-Q;ea7@4_jST8WwS+CfHaQK0Yjq$vuU zg>K9}@V)a*rDY-?)4IdeAx91{J#pPVZHlf@Cik0Il{NOue=73cOw~_A%Bae&#SHq9$(PsEgi-Fh{1r{^yyA+)f-I{+YWHOSXXFg`Q`VNg92Z;&lWE} z_w&U4t39g{%9P$+YtUb0bv$9}?}e=2>{xdRCa0$R`h@@Tz#+s0Kt zePL*cn$?jy&KZsmtRuN=FFZb(s2SUD64Cx`R`|lH3r%O&GpH|g{#kdevFwnE$`()0 z{RJM4d--Zae?%YPW)`|)-YB>tZo?$&XVouuJYd{n&olGZTIO>up;~vD<7Z#cVQ^d6 z^!nTlw$$$O&3rq!<($Q4uCwz$*&=j9sl-s6WtYJ0MN>aoA2`Rk|C^V}rr8r-INfdd z=GMA;Hfz=23pOoBzQ)ye*4GQCEW2~f>Vwb>jyc>m(Qmn1g>Gmb$Y0PGI)3nMxoRL7`m!XWms53v=8M8rbE?)W zH@>cKjMI!;uw;k4g;<2li?8JltDk9<6v#EXl_b>IehO_(VA5(jCw#f_?~4a%BA*ug zta@<&rPb8kj9MLk7hVezW;`$blMSN{hID$iw!s0F{dwoLcM><_dBApJM!I+|8APPVoIjk zhyNMzrmI(*dp5*nq{>V^R2^9)U$kxg_E3+-H?~V^{s}Zu+09rh_)Ynzilfe*mK@8> z^fOh9jF;_O@opn$>c449pA)6NPFs7Elh?z4*DbZMx}KVJc|qsX4SPN@Ri)G%wh~a@ z*C}WFJBodhQg%Rn)BBf?>N{?FO(-pMZC`aff_;+F-T?cak5{JcaQN90fAQ6fQ>H)youD^!Y9#pj@^GN-;oK4FHW+4a6e)a*~6FjKSS1o!OUgmgZGd3{&8Gd+uT^X zuW9M=V=KNtW+{0vi~Uu@;lOD>>h4`PqA6-`^<(X#hJn$$%)5>}`OT}rc8GJvOw4^L% zeR-v{`5dF(h8+>Sf8^F?81l@@X4uzu>PA~M`((#syAC``RXb&I&udrv)vqh!Ph33l zNPg9h9b!9d;^bRP#`gBFfa&xWtSFQH%mYzMehcm}i z{{BVr#rn64o?MP>-Ir4v_|!4~LGcT{eLG7Z_~}K?e0pw^Z$^I5o#}hJZgqtwoqW+O zS3kMT_4kDJ4}J?QulhG{tLPiqE1Q#<63u?Em29xdc4zwE6~5Qv>Euq8<3=g9J6KaM z>8NeVY1I<%Q~kNG!>_K>%J)Cts_WA2--0$8@K`Xf&nVdSY>ou$H=zPctAyBOSs(SB zey?`FWoPo%`<*o~QfoZxXv^^a$hL=RJ99d#l;!IC-(Q~7|5MtyaFqX&K^3ucEGHug-p@7lWg^V;UZpmZJGhnYF`V^-YZj)hg!JXFDgGu1LM> z-W?{TePip3gtAgE`Aybdb9fxjMpSM8Q^OVcdHMpU2QxJcKh~+{2PsxB^2xmSO6GrC z-Ff~+hO2#7{7RMI!hiM>&#SB-g14&cT|64sx^?!Eu_!KQ^nJ-Mr%Ai#taD??yau zdi{Voru*-Ugt}6W#WekjS||FMN$dzq$BgK?hG`23xm!%cti6HJ-oJcO>JR%* zx*p)Y#qh}7)H%)Hdeeh`F8*v3)%)$F0jtUM-wC@4s#*jhg35p1J8)Iw=zjkOi_=T> zSAClA_=&TtG^6#7?2N3xj<+|6@w(h+-Fj->Gy#9cOe6NMT{hE&?tE5cndY+p>)uB3 z#{Vvv-)Bg4Z0Zf4yvO&c>Xuou-ZEc%CI?=vKbx8H)w5~MH~Yxmu(b!L1YVI%;tylk zwse!wLbeih`?)z0_HoR8%*$9S-tu1DvDi2u%ied{LjB7vE%DpsKbYM-rDDdso$>iX z<9(YrKB}&B{@q!0Q>sE{Yv#G89S)pb$1i%#HqpLt_F1tl*Z=tbeg7?&@B9MZQFoe7EevC+`dhW-yEWqv zv6Hs*_H}NWVIKT&`U)4fMTc)Jne3uCqvKn|`LN$kirSXf7JXh-s9T@cFQ@UjRJ_Z7hxJZS<;>kFqZ^0~KU+I(@2{32P_JM|5xt|mP*d?54Sv{#dqwQgSeB@x zu?dNlJgJh|XGk~3RDcmYPx0z2xAl}to#DXh+QIahWQ$ zTqwTO%QUBJSMy!}nnzI`=R(;RC_N21%DS&9(vW-Z<~y0P9-?0y%)7bk1Lmgr=s1Th z?tXE=ZIYOtLA0F9%wv+*8mBnQT3z2IapRcP3^N_)=j*}+PTDYw&#*naLh{1-3m(fN zUN%}K&u!(3OJ2k&8Ghc`*?G}HRx$D53u%Akf=(1nJ>jPDxy;C<^{Jy&;p&9tT}awtT1&x+rXPp23%wO_&n$XPStk0I5bqdk8;A?*MZA)RJW#&eXsg1F%ePUUlZp^HV zHOgW;4$KKy<6!%?-fvg1%i=?<&s2Q3TodGuNvQV=TEke^&Gu$*s;rOBjstVDPWF@@ zX^L3AeC`iTh5fhA9T42HWM`?$;T;Da z9bT(!d5Bev|2^aM$JZvtJY?E@e66zOG1j#9X!k4z@rYkLN|&@1G%f{k1;isN?pOsK z-f`g2!)t=aS;ZQd3>bgphunK|#XxUEO5y8{&2nEWB>jYZnD(_qyU#JZ!tA}K`GkS% z>-rNO4>DgT%=xi`T`ZD)qGQ^Em3`Gr{L$=>k9e1dta?1<*y$DTkH#0g?g(c3dLsWx z$+?&Z)d$ucsECR2>%6k~9-rUuGwz3^7-HJhTjm(O|?b!pjAdc?D3m(xN91L?a< zv*)_5;P@_e$Lf)a!^YKDdapG~Y?k57W`AH56XWMpFA~1N_hMZ#^XrD7V{4WD0=l$s z>^>mBw%K!H=mCp_>IdpOcFb`Q2{*W)WhVV1SvJJs8pC!)|7$X@6XyK6yleCO-3L^! zF|sa_G<|bN(n3yr`edQC;RX$CSNbZ;Ump-!)X-8YXK>m|V)?fWZG(o~hL;D@*EX*f z+TLgJY>WAyM7jC`nOo~qCZ0Snz3_F1Fr%z*_)d8Xk%(N`1W6m#H~lT9XRRcTH=a8# z?#s~Ubaz71v&aVuDYBI(tR#+S`i6f#cx+A1KTdFRrcF5#p+>^;;q|rE__Po?z%Q{Zs1kV&4KkYeZfX=m%iNn$aqEP zvy%?zMUv*Uw<*WC|M~5BJhI=5C2Wa(flz_ehSY`|%NBPkZ=ZBmnR)rKm2L^HQVq3% zQU4P{E=O>(mgJc-s)oj`&nKwt=#8#(h+D0iQFC7BfoOu7 zfoR3;UriQ%lm0NfJr=F3*Y7!A!N9DmPl^F)%P_z-=hW%uOvWNz5%^Ffy3j aC&{jWZiU_wsf`Q_3=E#GelF{r5}E*0uPkQ( diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png index 25d345dcb7c162d8c9d83c32fa3cd490fb9f0e26..a9f0b08b2541c1dc00199a1a1e50cd4a69c592d5 100644 GIT binary patch delta 10643 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 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png index 9f5711ed2989f64c6886e12d87685a754c8453b1..3503924dcd02c6ddb7af765c17641528927c7812 100644 GIT binary patch delta 15889 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 Q0|Nttr>mdKI;Vst06&J=HUIzs delta 11563 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 diff --git a/scripts/generate_android_icons.sh b/scripts/generate_android_icons.sh new file mode 100755 index 000000000..3d988b85e --- /dev/null +++ b/scripts/generate_android_icons.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +# Android Icon Generator Script +# Generates notification icons and monochrome launcher icons from SVG source +# Usage: ./generate_android_icons.sh + +set -e + +# Configuration +SVG_SOURCE="assets/plezy.svg" +ANDROID_RES="android/app/src/main/res" +TEMP_DIR="/tmp/android_icons_$$" + +# Check if source SVG exists +if [ ! -f "$SVG_SOURCE" ]; then + echo "Error: $SVG_SOURCE not found" + exit 1 +fi + +# Check for required tools +if ! command -v rsvg-convert &> /dev/null; then + echo "Error: rsvg-convert not found. Install with: brew install librsvg" + exit 1 +fi + +if ! command -v magick &> /dev/null && ! command -v convert &> /dev/null; then + echo "Error: ImageMagick not found. Install with: brew install imagemagick" + exit 1 +fi + +# Create temp directory +mkdir -p "$TEMP_DIR" + +echo "🎨 Generating Android icons from $SVG_SOURCE..." + +# Function to generate white silhouette icon +generate_white_icon() { + local size=$1 + local output=$2 + + # Get SVG dimensions to detect if it's non-square + local svg_viewbox=$(grep -o 'viewBox="[^"]*"' "$SVG_SOURCE" | sed 's/viewBox="//;s/"//') + local svg_width=$(echo "$svg_viewbox" | awk '{print $3}') + local svg_height=$(echo "$svg_viewbox" | awk '{print $4}') + + # Convert SVG to PNG, preserving aspect ratio + if (( $(echo "$svg_width != $svg_height" | bc -l) )); then + # Non-square SVG: render at size and add padding to center it + rsvg-convert --keep-aspect-ratio --background-color=transparent "$SVG_SOURCE" -o "$TEMP_DIR/temp_unpadded.png" + + # Center the image in a square canvas with transparent padding + magick "$TEMP_DIR/temp_unpadded.png" \ + -resize "${size}x${size}" \ + -gravity center \ + -background transparent \ + -extent "${size}x${size}" \ + "$TEMP_DIR/temp.png" + else + # Square SVG: convert directly + rsvg-convert -w "$size" -h "$size" --background-color=transparent "$SVG_SOURCE" -o "$TEMP_DIR/temp.png" + fi + + # Convert to white silhouette: extract alpha, fill with white, apply alpha + magick "$TEMP_DIR/temp.png" \ + -alpha extract \ + "$TEMP_DIR/alpha_mask.png" + + magick -size "${size}x${size}" xc:white \ + "$TEMP_DIR/alpha_mask.png" \ + -alpha off \ + -compose copy-opacity \ + -composite \ + -define png:color-type=6 \ + "$output" + + echo " ✓ Generated $(basename $output) (${size}x${size}px)" +} + +# Generate Notification Icons (24dp base) +echo "" +echo "📱 Generating notification icons (ic_stat_notification.png)..." + +# Notification icon densities: 24dp base +# mdpi=1x, hdpi=1.5x, xhdpi=2x, xxhdpi=3x, xxxhdpi=4x +declare -A NOTIF_SIZES=( + ["mdpi"]=24 + ["hdpi"]=36 + ["xhdpi"]=48 + ["xxhdpi"]=72 + ["xxxhdpi"]=96 +) + +for density in "${!NOTIF_SIZES[@]}"; do + size=${NOTIF_SIZES[$density]} + output_dir="$ANDROID_RES/drawable-$density" + mkdir -p "$output_dir" + generate_white_icon "$size" "$output_dir/ic_stat_notification.png" +done + +# Generate Monochrome Launcher Icons (108dp base) +echo "" +echo "🚀 Generating monochrome launcher icons (ic_launcher_monochrome.png)..." + +# Monochrome launcher icon densities: 108dp base +# mdpi=1x, hdpi=1.5x, xhdpi=2x, xxhdpi=3x, xxxhdpi=4x +declare -A MONO_SIZES=( + ["mdpi"]=108 + ["hdpi"]=162 + ["xhdpi"]=216 + ["xxhdpi"]=324 + ["xxxhdpi"]=432 +) + +for density in "${!MONO_SIZES[@]}"; do + size=${MONO_SIZES[$density]} + output_dir="$ANDROID_RES/mipmap-$density" + mkdir -p "$output_dir" + generate_white_icon "$size" "$output_dir/ic_launcher_monochrome.png" +done + +# Clean up +rm -rf "$TEMP_DIR" + +echo "" +echo "✅ All icons generated successfully!" +echo "" +echo "Icon locations:" +echo " • Notification icons: $ANDROID_RES/drawable-*/ic_stat_notification.png" +echo " • Monochrome icons: $ANDROID_RES/mipmap-*/ic_launcher_monochrome.png" +echo "" +echo "To apply changes, rebuild the app: flutter clean && flutter build apk" From b6234120dde4707bd9f44ea383ca1908fd893205 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:53:44 +0100 Subject: [PATCH 05/25] fix: android crash --- .../drawable-hdpi/ic_stat_notification.png | Bin 1122 -> 1122 bytes .../drawable-mdpi/ic_stat_notification.png | Bin 820 -> 820 bytes .../drawable-xhdpi/ic_stat_notification.png | Bin 1445 -> 1445 bytes .../drawable-xxhdpi/ic_stat_notification.png | Bin 2181 -> 2181 bytes .../drawable-xxxhdpi/ic_stat_notification.png | Bin 2843 -> 2843 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 4708 -> 4708 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 3052 -> 3052 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 6581 -> 6581 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 10735 -> 10735 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 15940 -> 15940 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-hdpi/ic_stat_notification.png index fbfeb059386405d5cf5e5e9f5b8b1d563d2469a8..d3e232cb01c69523f021ae463208141c229a1a6a 100644 GIT binary patch delta 85 zcmaFF@rYwW4x5pAT=8z7jYS2_{H6w0rl#5k237_JaXg8|lRq*mA&cn;-aJ29iA7Z! WNnM4@4*>=S1_n=8KbLh*2~7Y*tQZsk delta 85 zcmaFF@rYwW4x5(MqMPw6HWn2y^P3o1nObNY7+4t?{93^8H~Ay860%tL-|e3!E3v3b XBdNRdVwx@k0|SGntDnm{r-UW|&_)|d diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-mdpi/ic_stat_notification.png index 5c7c824083faca155f367a8aaaac95c59d6c931a..66c57139d20910e20cf96d36b14f45df14d8a65e 100644 GIT binary patch delta 85 zcmdnOwuNm%4x5pAT=8z7jYUpO{H6w0rl#5k237_JaXg8|lMgc~A&cn;-aJ3~3zMoe WlDZ0)9|8;v3=E#GelF{r5}E)gEf^*M delta 85 zcmdnOwuNm%4x5(MqMPw6HWoQC@tYV~nObNY7+4t?{93^8H~BD=60%tL-|e3!e_>LU XMpAd@#WY<81_lOCS3j3^P68^C!4S;A&cn;-aJ1!l~q+5 WNnM4@4*>=S1_n=8KbLh*2~7YLB^PY~ delta 85 zcmZ3=y_9=G4x5(MqMPw6HWqDW;Wsg|GPTe)Ft9Q(__cuFZ?Xxi60%tL-|e3!r?RR_ XBdNRdVwx@k0|SGntDnm{r-UW|pT8QT diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification.png index 8711c8628a05edc289deb1d467bc1426ee1b3d65..8949925fc622f0a9da5310085657752f380b6dc4 100644 GIT binary patch delta 85 zcmZn_Y!#f4!)9b2SG?P2W6@-Gep3T0Q&VjN11kfAIG)7f$s!y|$YT0|H_uOYgTe~DWM4fePS9I diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_notification.png index f0ecd616896b6f9548b91db53a3f0c2ac6035c3b..be6ea229816ff13413ee680244cb212c5928d75b 100644 GIT binary patch delta 85 zcmbO&Hd|~$4x5pAT=8z7jYXPV{H6w0rl#5k237_JaXg8|lUH#mA&cn;-aJ3~HkYb2 WlDZ0)9|8;v3=E#GelF{r5}E)P^%vm) delta 85 zcmbO&Hd|~$4x5(MqMPw6HWq1e@tYV~nObNY7+4t?{93^8H+dD860%tL-|e3!-{w-4 XMpAd@#WY<81_lOCS3j3^P6=S1_n=8KbLh*2~7ZDdKiEJ delta 85 zcmaE&@_)QJ0Oii^746F8^Cs%VTA&cn;-aJ2f4Y#T^ WlDZ0)9|8;v3=E#GelF{r5}E*bD;W9! delta 85 zcmaDO{ziO44x5(MqMPw6HWvNj;x{p}GPTe)Ft9Q(__cuFZ*n!a60%tL-|e3!ui;je XMpAd@#WY<81_lOCS3j3^P64x5pAT=8z7jYWSo_)QJ0Oii^746F8^C)a5zA&cn;-aJ2fy{4)( WlDZ0)9|8;v3=E#GelF{r5}E+CNg3M! delta 85 zcmaDK{62U>4x5(MqMPw6HWvNW;5RX{GPTe)Ft9Q(__cuFZ*rZc60%tL-|e3!uh&$S XMpAd@#WY<81_lOCS3j3^P6=S1_n=8KbLh*2~7ZuCK(g} delta 85 zcmX?7bEIZM4x5(MqMPw6HWmfh@|zf0nObNY7+4t?{93^8H~F%y60%tL-|e3!v)HLh XBdNRdVwx@k0|SGntDnm{r-UW|7tb6@ From fac7aed1e73643cbb4b8a9c0dc16ceaae75589c8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:50:09 +0100 Subject: [PATCH 06/25] feat: shuffle settings --- lib/providers/playback_state_provider.dart | 33 ++++++++++- lib/providers/settings_provider.dart | 33 +++++++++++ lib/screens/settings_screen.dart | 66 ++++++++++++++++++++++ lib/screens/video_player_screen.dart | 22 ++++++-- lib/services/settings_service.dart | 35 ++++++++++++ lib/utils/shuffle_play_helper.dart | 54 ++++++++++++++---- 6 files changed, 227 insertions(+), 16 deletions(-) diff --git a/lib/providers/playback_state_provider.dart b/lib/providers/playback_state_provider.dart index 8426817b7..b61ce5dff 100644 --- a/lib/providers/playback_state_provider.dart +++ b/lib/providers/playback_state_provider.dart @@ -25,7 +25,8 @@ class PlaybackStateProvider with ChangeNotifier { /// Gets the next episode in the shuffle queue. /// Returns null if queue is exhausted or current episode is not in queue. - PlexMetadata? getNextEpisode(String currentEpisodeKey) { + /// [loopQueue] - If true, restart from beginning when queue is exhausted + PlexMetadata? getNextEpisode(String currentEpisodeKey, {bool loopQueue = false}) { if (_shuffleQueue.isEmpty) return null; // Find current episode in queue @@ -42,6 +43,11 @@ class PlaybackStateProvider with ChangeNotifier { // Check if there's a next episode if (currentIndex + 1 >= _shuffleQueue.length) { // Queue exhausted + if (loopQueue && _shuffleQueue.isNotEmpty) { + // Loop back to beginning + _currentIndex = 0; + return _shuffleQueue[_currentIndex]; + } return null; } @@ -49,6 +55,31 @@ class PlaybackStateProvider with ChangeNotifier { return _shuffleQueue[_currentIndex]; } + /// Gets the previous episode in the shuffle queue. + /// Returns null if at the beginning of the queue or current episode is not in queue. + PlexMetadata? getPreviousEpisode(String currentEpisodeKey) { + if (_shuffleQueue.isEmpty) return null; + + // Find current episode in queue + final currentIndex = _shuffleQueue.indexWhere( + (ep) => ep.ratingKey == currentEpisodeKey, + ); + + if (currentIndex == -1) { + // Current episode not in queue + return null; + } + + // Check if there's a previous episode + if (currentIndex <= 0) { + // At the beginning of queue + return null; + } + + _currentIndex = currentIndex - 1; + return _shuffleQueue[_currentIndex]; + } + /// Clears the shuffle queue and exits shuffle mode void clearShuffle() { _shuffleQueue = []; diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index cd9ffa3ca..61e98b6a0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -7,6 +7,9 @@ class SettingsProvider extends ChangeNotifier { ViewMode _viewMode = ViewMode.grid; bool _useSeasonPoster = false; bool _showHeroSection = true; + bool _shuffleUnwatchedOnly = true; + bool _shuffleOrderNavigation = true; + bool _shuffleLoopQueue = false; SettingsProvider() { _initializeSettings(); @@ -18,6 +21,9 @@ class SettingsProvider extends ChangeNotifier { _viewMode = _settingsService.getViewMode(); _useSeasonPoster = _settingsService.getUseSeasonPoster(); _showHeroSection = _settingsService.getShowHeroSection(); + _shuffleUnwatchedOnly = _settingsService.getShuffleUnwatchedOnly(); + _shuffleOrderNavigation = _settingsService.getShuffleOrderNavigation(); + _shuffleLoopQueue = _settingsService.getShuffleLoopQueue(); notifyListeners(); } @@ -25,6 +31,9 @@ class SettingsProvider extends ChangeNotifier { ViewMode get viewMode => _viewMode; bool get useSeasonPoster => _useSeasonPoster; bool get showHeroSection => _showHeroSection; + bool get shuffleUnwatchedOnly => _shuffleUnwatchedOnly; + bool get shuffleOrderNavigation => _shuffleOrderNavigation; + bool get shuffleLoopQueue => _shuffleLoopQueue; Future setLibraryDensity(LibraryDensity density) async { if (_libraryDensity != density) { @@ -58,6 +67,30 @@ class SettingsProvider extends ChangeNotifier { } } + Future setShuffleUnwatchedOnly(bool value) async { + if (_shuffleUnwatchedOnly != value) { + _shuffleUnwatchedOnly = value; + await _settingsService.setShuffleUnwatchedOnly(value); + notifyListeners(); + } + } + + Future setShuffleOrderNavigation(bool value) async { + if (_shuffleOrderNavigation != value) { + _shuffleOrderNavigation = value; + await _settingsService.setShuffleOrderNavigation(value); + notifyListeners(); + } + } + + Future setShuffleLoopQueue(bool value) async { + if (_shuffleLoopQueue != value) { + _shuffleLoopQueue = value; + await _settingsService.setShuffleLoopQueue(value); + notifyListeners(); + } + } + String get libraryDensityDisplayName { switch (_libraryDensity) { case LibraryDensity.compact: diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 9e885e357..9af5bca39 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -75,6 +75,8 @@ class _SettingsScreenState extends State { const SizedBox(height: 24), _buildVideoPlaybackSection(), const SizedBox(height: 24), + _buildShufflePlaySection(), + const SizedBox(height: 24), _buildKeyboardShortcutsSection(), const SizedBox(height: 24), _buildAdvancedSection(), @@ -248,6 +250,70 @@ class _SettingsScreenState extends State { ); } + Widget _buildShufflePlaySection() { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Shuffle Play', + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Consumer( + builder: (context, settingsProvider, child) { + return SwitchListTile( + secondary: const Icon(Icons.visibility_off), + title: const Text('Unwatched Only'), + subtitle: const Text( + 'Only include unwatched episodes in shuffle queue', + ), + value: settingsProvider.shuffleUnwatchedOnly, + onChanged: (value) async { + await settingsProvider.setShuffleUnwatchedOnly(value); + }, + ); + }, + ), + Consumer( + builder: (context, settingsProvider, child) { + return SwitchListTile( + secondary: const Icon(Icons.shuffle), + title: const Text('Shuffle Order Navigation'), + subtitle: const Text( + 'Next/previous buttons follow shuffled order', + ), + value: settingsProvider.shuffleOrderNavigation, + onChanged: (value) async { + await settingsProvider.setShuffleOrderNavigation(value); + }, + ); + }, + ), + Consumer( + builder: (context, settingsProvider, child) { + return SwitchListTile( + secondary: const Icon(Icons.loop), + title: const Text('Loop Shuffle Queue'), + subtitle: const Text( + 'Restart queue when reaching the end', + ), + value: settingsProvider.shuffleLoopQueue, + onChanged: (value) async { + await settingsProvider.setShuffleLoopQueue(value); + }, + ); + }, + ), + ], + ), + ); + } + Widget _buildKeyboardShortcutsSection() { return Card( child: Column( diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index a2ee3e759..e65609d88 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -9,6 +9,7 @@ import '../models/plex_metadata.dart'; import '../models/plex_user_profile.dart'; import '../providers/plex_client_provider.dart'; import '../providers/playback_state_provider.dart'; +import '../providers/settings_provider.dart'; import '../utils/provider_extensions.dart'; import '../widgets/video_controls/video_controls.dart'; import '../utils/language_codes.dart'; @@ -323,16 +324,29 @@ class VideoPlayerScreenState extends State { if (client == null) return; final playbackState = context.read(); + final settingsProvider = context.read(); PlexMetadata? next; PlexMetadata? previous; // Check if shuffle mode is active if (playbackState.isShuffleActive) { - // Get next episode from shuffle queue - next = playbackState.getNextEpisode(widget.metadata.ratingKey); - // No previous episode in shuffle mode - previous = null; + // Get settings + final shuffleOrderNavigation = settingsProvider.shuffleOrderNavigation; + final loopQueue = settingsProvider.shuffleLoopQueue; + + if (shuffleOrderNavigation) { + // Use shuffled order for next/previous + next = playbackState.getNextEpisode( + widget.metadata.ratingKey, + loopQueue: loopQueue, + ); + previous = playbackState.getPreviousEpisode(widget.metadata.ratingKey); + } else { + // Use chronological order even in shuffle mode + next = await client.findAdjacentEpisode(widget.metadata, 1); + previous = await client.findAdjacentEpisode(widget.metadata, -1); + } } else { // Use normal sequential episode loading next = await client.findAdjacentEpisode(widget.metadata, 1); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index aefd06c48..c24ff73fa 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -37,6 +37,9 @@ class SettingsService { static const String _keySubtitleBorderColor = 'subtitle_border_color'; static const String _keySubtitleBackgroundColor = 'subtitle_background_color'; static const String _keySubtitleBackgroundOpacity = 'subtitle_background_opacity'; + static const String _keyShuffleUnwatchedOnly = 'shuffle_unwatched_only'; + static const String _keyShuffleOrderNavigation = 'shuffle_order_navigation'; + static const String _keyShuffleLoopQueue = 'shuffle_loop_queue'; static SettingsService? _instance; late SharedPreferences _prefs; @@ -777,6 +780,35 @@ class SettingsService { } } + // Shuffle Play Settings + + /// Shuffle Unwatched Only - Filter shuffle queue to unwatched episodes only + Future setShuffleUnwatchedOnly(bool enabled) async { + await _prefs.setBool(_keyShuffleUnwatchedOnly, enabled); + } + + bool getShuffleUnwatchedOnly() { + return _prefs.getBool(_keyShuffleUnwatchedOnly) ?? true; // Default: true + } + + /// Shuffle Order Navigation - Next/previous buttons follow shuffled order + Future setShuffleOrderNavigation(bool enabled) async { + await _prefs.setBool(_keyShuffleOrderNavigation, enabled); + } + + bool getShuffleOrderNavigation() { + return _prefs.getBool(_keyShuffleOrderNavigation) ?? true; // Default: true + } + + /// Shuffle Loop Queue - Restart queue when reaching the end + Future setShuffleLoopQueue(bool enabled) async { + await _prefs.setBool(_keyShuffleLoopQueue, enabled); + } + + bool getShuffleLoopQueue() { + return _prefs.getBool(_keyShuffleLoopQueue) ?? false; // Default: false + } + // Reset all settings to defaults Future resetAllSettings() async { await Future.wait([ @@ -805,6 +837,9 @@ class SettingsService { _prefs.remove(_keySubtitleBorderColor), _prefs.remove(_keySubtitleBackgroundColor), _prefs.remove(_keySubtitleBackgroundOpacity), + _prefs.remove(_keyShuffleUnwatchedOnly), + _prefs.remove(_keyShuffleOrderNavigation), + _prefs.remove(_keyShuffleLoopQueue), ]); } diff --git a/lib/utils/shuffle_play_helper.dart b/lib/utils/shuffle_play_helper.dart index fca5eb047..8f3ab23e4 100644 --- a/lib/utils/shuffle_play_helper.dart +++ b/lib/utils/shuffle_play_helper.dart @@ -2,14 +2,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/plex_metadata.dart'; import '../providers/playback_state_provider.dart'; +import '../providers/settings_provider.dart'; import '../utils/provider_extensions.dart'; import '../utils/video_player_navigation.dart'; /// Handle shuffle play action for shows and seasons /// -/// Fetches all unwatched episodes, shuffles them, and starts playback -/// from the first shuffled episode. The shuffle queue is stored in the -/// PlaybackStateProvider for continuous shuffle playback. +/// Fetches episodes based on user settings (unwatched only or including watched), +/// shuffles them, and starts playback from the first shuffled episode. +/// The shuffle queue is stored in the PlaybackStateProvider for continuous shuffle playback. Future handleShufflePlay( BuildContext context, PlexMetadata metadata, @@ -18,8 +19,12 @@ Future handleShufflePlay( if (client == null) return; final playbackState = context.read(); + final settingsProvider = context.read(); final itemType = metadata.type.toLowerCase(); + // Get shuffle setting + final unwatchedOnly = settingsProvider.shuffleUnwatchedOnly; + try { // Show loading indicator if (context.mounted) { @@ -31,17 +36,44 @@ Future handleShufflePlay( ); } - // Get unwatched episodes based on type + // Get episodes based on type and settings List episodes; if (itemType == 'show') { - episodes = await client.getAllUnwatchedEpisodes( - metadata.ratingKey, - ); + if (unwatchedOnly) { + // Get only unwatched episodes + episodes = await client.getAllUnwatchedEpisodes( + metadata.ratingKey, + ); + } else { + // Get all episodes from all seasons + final allEpisodes = []; + final seasons = await client.getChildren(metadata.ratingKey); + + for (final season in seasons) { + if (season.type == 'season') { + final seasonEpisodes = await client.getChildren(season.ratingKey); + final episodesOnly = seasonEpisodes + .where((ep) => ep.type == 'episode') + .toList(); + allEpisodes.addAll(episodesOnly); + } + } + episodes = allEpisodes; + } } else { // season - episodes = await client.getUnwatchedEpisodesInSeason( - metadata.ratingKey, - ); + if (unwatchedOnly) { + // Get only unwatched episodes + episodes = await client.getUnwatchedEpisodesInSeason( + metadata.ratingKey, + ); + } else { + // Get all episodes in season + final seasonEpisodes = await client.getChildren(metadata.ratingKey); + episodes = seasonEpisodes + .where((ep) => ep.type == 'episode') + .toList(); + } } // Close loading indicator @@ -52,7 +84,7 @@ Future handleShufflePlay( if (episodes.isEmpty) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No unwatched episodes found')), + const SnackBar(content: Text('No episodes found')), ); } return; From 0d4f7a9e9fc2eba5e265f4841d0ae083395eb13f Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 8 Nov 2025 08:35:52 +0100 Subject: [PATCH 07/25] refactor: remove duplicate audio sessions --- lib/screens/video_player_screen.dart | 50 ++----------------------- lib/services/media_service_manager.dart | 2 + pubspec.lock | 11 +++--- pubspec.yaml | 6 ++- 4 files changed, 16 insertions(+), 53 deletions(-) diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e65609d88..c480c1db6 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:provider/provider.dart'; -import 'package:audio_session/audio_session.dart'; import '../models/plex_metadata.dart'; import '../models/plex_user_profile.dart'; import '../providers/plex_client_provider.dart'; @@ -167,9 +166,6 @@ class VideoPlayerScreenState extends State { final savedVolume = settingsService.getVolume(); player!.setVolume(savedVolume); - // Initialize audio session for proper OS integration - await _initializeAudioSession(); - // Update media service manager with new player await _updateMediaService(); @@ -237,48 +233,6 @@ class VideoPlayerScreenState extends State { } } - Future _initializeAudioSession() async { - try { - final session = await AudioSession.instance; - await session.configure(const AudioSessionConfiguration( - avAudioSessionCategory: AVAudioSessionCategory.playback, - avAudioSessionMode: AVAudioSessionMode.moviePlayback, - androidAudioAttributes: AndroidAudioAttributes( - contentType: AndroidAudioContentType.movie, - usage: AndroidAudioUsage.media, - ), - androidAudioFocusGainType: AndroidAudioFocusGainType.gain, - )); - - // Handle interruptions (phone calls, other apps, etc.) - session.interruptionEventStream.listen((event) { - if (event.begin) { - // Pause on interruption - player?.pause(); - appLogger.i('Playback paused due to interruption'); - } else { - // Resume after interruption if needed (user can manually play) - switch (event.type) { - case AudioInterruptionType.pause: - case AudioInterruptionType.duck: - // Don't auto-resume for these types - break; - case AudioInterruptionType.unknown: - break; - } - } - }); - - // Handle audio becoming noisy (headphones unplugged) - session.becomingNoisyEventStream.listen((_) { - player?.pause(); - appLogger.i('Playback paused due to audio becoming noisy (headphones unplugged?)'); - }); - } catch (e) { - appLogger.w('Failed to configure audio session', error: e); - } - } - Future _updateMediaService() async { if (!mounted) return; @@ -341,7 +295,9 @@ class VideoPlayerScreenState extends State { widget.metadata.ratingKey, loopQueue: loopQueue, ); - previous = playbackState.getPreviousEpisode(widget.metadata.ratingKey); + previous = playbackState.getPreviousEpisode( + widget.metadata.ratingKey, + ); } else { // Use chronological order even in shuffle mode next = await client.findAdjacentEpisode(widget.metadata, 1); diff --git a/lib/services/media_service_manager.dart b/lib/services/media_service_manager.dart index 9a3b383f7..15c1e2593 100644 --- a/lib/services/media_service_manager.dart +++ b/lib/services/media_service_manager.dart @@ -40,6 +40,8 @@ class MediaServiceManager { androidNotificationOngoing: false, androidStopForegroundOnPause: true, androidNotificationIcon: 'drawable/ic_stat_notification', + // Configure audio session for proper media playback + preloadArtwork: true, ), ); diff --git a/pubspec.lock b/pubspec.lock index 6ee9026ef..3dbbbdf21 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -44,10 +44,11 @@ packages: audio_service: dependency: "direct main" description: - name: audio_service - sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 - url: "https://pub.dev" - source: hosted + path: audio_service + ref: "4ca42f391183854095188a5a10fa6b59cd641e68" + resolved-ref: "4ca42f391183854095188a5a10fa6b59cd641e68" + url: "https://github.com/marckornberger/audio_service.git" + source: git version: "0.18.18" audio_service_platform_interface: dependency: transitive @@ -66,7 +67,7 @@ packages: source: hosted version: "0.1.4" audio_session: - dependency: "direct main" + dependency: transitive description: name: audio_session sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" diff --git a/pubspec.yaml b/pubspec.yaml index 09dde9e58..9712efb53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,6 @@ dependencies: hotkey_manager: ^0.2.3 flex_color_picker: ^3.6.0 audio_service: ^0.18.18 - audio_session: ^0.2.1 qr_flutter: ^4.1.0 dependency_overrides: @@ -40,6 +39,11 @@ dependency_overrides: url: https://github.com/edde746/media-kit ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 path: libs/universal/media_kit_libs_video + audio_service: + git: + url: https://github.com/marckornberger/audio_service.git + ref: 4ca42f391183854095188a5a10fa6b59cd641e68 + path: audio_service dev_dependencies: flutter_test: From 6228eb26906e734198dfe2c1359be126ca10c714 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 8 Nov 2025 10:03:52 +0100 Subject: [PATCH 08/25] refactor: rewrite os media controls # Conflicts: # lib/screens/video_player_screen.dart # lib/services/media_service_manager.dart # pubspec.yaml --- android/app/src/main/AndroidManifest.xml | 20 +- .../flutter_application_1/MainActivity.kt | 4 +- lib/main.dart | 4 - lib/screens/video_player_screen.dart | 105 ++-- lib/services/audio_service_manager.dart | 186 +++++++ lib/services/media_kit_audio_handler.dart | 456 ++++++++++-------- lib/services/media_service_manager.dart | 123 ----- pubspec.lock | 12 +- pubspec.yaml | 13 +- 9 files changed, 507 insertions(+), 416 deletions(-) create mode 100644 lib/services/audio_service_manager.dart delete mode 100644 lib/services/media_service_manager.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 20294c8f0..0dfda6f04 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,10 +3,10 @@ - + + - @@ -46,22 +46,18 @@ android:name="flutterEmbedding" android:value="2" /> - - + + android:exported="true" tools:ignore="Instantiatable"> - - + + diff --git a/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt b/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt index 699f962ef..923422cc1 100644 --- a/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/flutter_application_1/MainActivity.kt @@ -1,5 +1,5 @@ package com.edde746.plezy -import com.ryanheise.audioservice.AudioServiceFragmentActivity +import com.ryanheise.audioservice.AudioServiceActivity -class MainActivity : AudioServiceFragmentActivity() +class MainActivity : AudioServiceActivity() diff --git a/lib/main.dart b/lib/main.dart index ac908e2f5..8aab1ae39 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'services/macos_titlebar_service.dart'; import 'services/fullscreen_state_manager.dart'; import 'services/update_service.dart'; import 'services/settings_service.dart'; -import 'services/media_service_manager.dart'; import 'providers/user_profile_provider.dart'; import 'providers/plex_client_provider.dart'; import 'providers/theme_provider.dart'; @@ -43,9 +42,6 @@ void main() async { // Initialize MediaKit MediaKit.ensureInitialized(); - // Initialize OS media controls - await MediaServiceManager.instance.initialize(); - // Note: Orientation will be set dynamically based on device type in MainApp await StorageService.getInstance(); diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index c480c1db6..a1d3de421 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -14,7 +14,7 @@ import '../widgets/video_controls/video_controls.dart'; import '../utils/language_codes.dart'; import '../utils/app_logger.dart'; import '../services/settings_service.dart'; -import '../services/media_service_manager.dart'; +import '../services/audio_service_manager.dart'; import '../utils/orientation_helper.dart'; import '../utils/video_player_navigation.dart'; import '../utils/platform_detector.dart'; @@ -166,9 +166,6 @@ class VideoPlayerScreenState extends State { final savedVolume = settingsService.getVolume(); player!.setVolume(savedVolume); - // Update media service manager with new player - await _updateMediaService(); - // Notify that player is ready if (mounted) { setState(() { @@ -223,6 +220,9 @@ class VideoPlayerScreenState extends State { // Load next/previous episodes _loadAdjacentEpisodes(); + + // Initialize audio service for OS media controls + _initializeAudioService(); } catch (e) { appLogger.e('Failed to initialize player', error: e); if (mounted) { @@ -233,39 +233,6 @@ class VideoPlayerScreenState extends State { } } - Future _updateMediaService() async { - if (!mounted) return; - - try { - final mediaService = MediaServiceManager.instance; - - // Build thumbnail URL if available - String? thumbnailUrl; - final clientProvider = context.plexClient; - final client = clientProvider.client; - if (widget.metadata.thumb != null && client != null) { - final baseUrl = client.config.baseUrl; - final token = client.config.token; - if (token != null) { - thumbnailUrl = '$baseUrl${widget.metadata.thumb}?X-Plex-Token=$token'; - } - } - - // CRITICAL: Set mediaItem FIRST before updating player - // Android requires mediaItem to exist before playbackState broadcasts - mediaService.updateMediaItem(widget.metadata, thumbnailUrl); - - if (!mounted) return; - - await mediaService.updatePlayer( - player: player, - onNext: _playNext, - onPrevious: _playPrevious, - ); - } catch (e) { - appLogger.w('Failed to update media service', error: e); - } - } Future _loadAdjacentEpisodes() async { if (widget.metadata.type.toLowerCase() != 'episode') { @@ -295,9 +262,7 @@ class VideoPlayerScreenState extends State { widget.metadata.ratingKey, loopQueue: loopQueue, ); - previous = playbackState.getPreviousEpisode( - widget.metadata.ratingKey, - ); + previous = playbackState.getPreviousEpisode(widget.metadata.ratingKey); } else { // Use chronological order even in shuffle mode next = await client.findAdjacentEpisode(widget.metadata, 1); @@ -315,17 +280,51 @@ class VideoPlayerScreenState extends State { _previousEpisode = previous; }); - // Update media service navigation controls - MediaServiceManager.instance.updateNavigationActions( - hasNext: _nextEpisode != null, - hasPrevious: _previousEpisode != null, - ); + // Update audio handler navigation availability + _updateAudioHandlerNavigation(); } } catch (e) { // Silently handle errors } } + Future _initializeAudioService() async { + try { + if (player == null) return; + + final clientProvider = context.plexClient; + final client = clientProvider.client; + if (client == null) return; + + final audioManager = AudioServiceManager.instance; + + // Initialize audio service (will only happen once) + await audioManager.initialize( + player: player!, + plexServerUrl: client.config.baseUrl, + authToken: client.config.token ?? '', + onSkipToNext: _nextEpisode != null ? _playNext : null, + onSkipToPrevious: _previousEpisode != null ? _playPrevious : null, + ); + + // Update media item with current content + audioManager.updateMediaItem(widget.metadata); + + appLogger.d('Audio service initialized/updated successfully'); + } catch (e) { + appLogger.e('Failed to initialize audio service', error: e); + // Continue playback even if audio service fails + } + } + + void _updateAudioHandlerNavigation() { + final audioManager = AudioServiceManager.instance; + audioManager.updateNavigation( + onNext: _nextEpisode != null ? _playNext : null, + onPrevious: _previousEpisode != null ? _playPrevious : null, + ); + } + Future _startPlayback() async { try { final clientProvider = context.plexClient; @@ -446,10 +445,6 @@ class VideoPlayerScreenState extends State { // Start playback after seeking await player!.play(); - // Force media service state update to trigger Android notification - // This ensures the notification appears even if streams don't fire reliably - MediaServiceManager.instance.forceStateUpdate(); - // Wait for tracks to be loaded, then apply preferred tracks _waitForTracksAndApply(); } else { @@ -525,8 +520,16 @@ class VideoPlayerScreenState extends State { _logSubscription?.cancel(); _errorSubscription?.cancel(); - // Stop media service and clear OS controls - MediaServiceManager.instance.stop(); + // Handle audio service based on navigation intent + if (_isReplacingWithVideo) { + // Switching to another episode: pause but keep notification visible + // The new VideoPlayerScreen will update the notification + AudioServiceManager.instance.pause(); + } else { + // Truly exiting player: clear notification but keep singleton alive + // This allows controls to reappear when playing again + AudioServiceManager.instance.clearNotification(); + } // Send final stopped state _sendProgress('stopped'); diff --git a/lib/services/audio_service_manager.dart b/lib/services/audio_service_manager.dart new file mode 100644 index 000000000..778d7688c --- /dev/null +++ b/lib/services/audio_service_manager.dart @@ -0,0 +1,186 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:media_kit/media_kit.dart'; +import '../models/plex_metadata.dart'; +import '../utils/app_logger.dart'; +import 'media_kit_audio_handler.dart'; + +/// Global singleton manager for audio service +/// Ensures AudioService.init() is only called once and the handler is reused +class AudioServiceManager { + static AudioServiceManager? _instance; + MediaKitAudioHandler? _handler; + bool _isInitialized = false; + bool _isInitializing = false; + + AudioServiceManager._(); + + static AudioServiceManager get instance { + _instance ??= AudioServiceManager._(); + return _instance!; + } + + /// Initialize the audio service with the given player and configuration + /// This should only be called once - subsequent calls will reuse the existing handler + Future initialize({ + required Player player, + required String plexServerUrl, + required String authToken, + Future Function()? onSkipToNext, + Future Function()? onSkipToPrevious, + }) async { + // If already initialized, update the player reference and callbacks + if (_isInitialized && _handler != null) { + appLogger.d('Audio service already initialized, updating player reference and callbacks'); + + // Update the player reference (critical for episode switching) + await _handler!.updatePlayer(player); + + // Update navigation callbacks + _handler!.onSkipToNext = onSkipToNext; + _handler!.onSkipToPrevious = onSkipToPrevious; + + return; + } + + // Prevent concurrent initialization + if (_isInitializing) { + appLogger.d('Audio service initialization already in progress, waiting...'); + // Wait for initialization to complete + while (_isInitializing) { + await Future.delayed(const Duration(milliseconds: 100)); + } + return; + } + + try { + _isInitializing = true; + appLogger.d('Initializing audio service for the first time'); + + final handler = await AudioService.init( + builder: () => MediaKitAudioHandler( + player: player, + plexServerUrl: plexServerUrl, + authToken: authToken, + onSkipToNext: onSkipToNext, + onSkipToPrevious: onSkipToPrevious, + ), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.edde746.plezy.audio', + androidNotificationChannelName: 'Plezy Playback', + androidNotificationOngoing: true, + androidShowNotificationBadge: true, + ), + ); + + _handler = handler; + _isInitialized = true; + appLogger.d('Audio service initialized successfully'); + } catch (e) { + appLogger.e('Failed to initialize audio service', error: e); + rethrow; + } finally { + _isInitializing = false; + } + } + + /// Update the current media item being played + void updateMediaItem(PlexMetadata metadata) { + if (_handler == null) { + appLogger.w('Cannot update media item: audio handler not initialized'); + return; + } + _handler!.updateCurrentMediaItem(metadata); + } + + /// Update navigation callbacks for next/previous episode + void updateNavigation({ + Future Function()? onNext, + Future Function()? onPrevious, + }) { + if (_handler == null) { + appLogger.w('Cannot update navigation: audio handler not initialized'); + return; + } + _handler!.updateNavigationCallbacks( + onNext: onNext, + onPrevious: onPrevious, + ); + } + + /// Pause playback but keep the media session active + Future pause() async { + if (_handler == null) { + appLogger.w('Cannot pause: audio handler not initialized'); + return; + } + await _handler!.pause(); + } + + /// Clear the notification but keep the audio service singleton alive + /// This is used when exiting the video player but not closing the app + Future clearNotification() async { + if (_handler == null) { + appLogger.w('Cannot clear notification: audio handler not initialized'); + return; + } + + appLogger.d('Clearing notification while keeping audio service alive'); + + // Broadcast idle state to remove notification + _handler!.playbackState.add(PlaybackState( + processingState: AudioProcessingState.idle, + playing: false, + controls: [], + systemActions: const {}, + updatePosition: Duration.zero, + bufferedPosition: Duration.zero, + speed: 1.0, + )); + + // Clear media item + _handler!.mediaItem.add(null); + + // IMPORTANT: DON'T reset _isInitialized or _handler + // Keep the singleton alive for next playback + + appLogger.d('Notification cleared, audio service remains initialized'); + } + + /// Stop playback completely and remove the notification + /// This should only be called when the app is closing or user explicitly stops + Future shutdown() async { + if (_handler == null) { + appLogger.w('Cannot shutdown: audio handler not initialized'); + return; + } + + appLogger.d('Shutting down audio service and removing notification'); + + // Broadcast idle state to remove notification + _handler!.playbackState.add(PlaybackState( + processingState: AudioProcessingState.idle, + playing: false, + controls: [], + systemActions: const {}, + updatePosition: Duration.zero, + bufferedPosition: Duration.zero, + speed: 1.0, + )); + + // Clear media item + _handler!.mediaItem.add(null); + + // Dispose the handler + await _handler!.dispose(); + + _handler = null; + _isInitialized = false; + appLogger.d('Audio service shutdown complete'); + } + + /// Check if the audio service is initialized + bool get isInitialized => _isInitialized; + + /// Get the current handler (for advanced use cases) + MediaKitAudioHandler? get handler => _handler; +} diff --git a/lib/services/media_kit_audio_handler.dart b/lib/services/media_kit_audio_handler.dart index e77c26f4f..80b624b25 100644 --- a/lib/services/media_kit_audio_handler.dart +++ b/lib/services/media_kit_audio_handler.dart @@ -1,117 +1,195 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:audio_service/audio_service.dart'; import 'package:media_kit/media_kit.dart'; +import 'package:rxdart/rxdart.dart'; import '../models/plex_metadata.dart'; import '../utils/app_logger.dart'; -/// AudioHandler that bridges media_kit Player with OS media controls +/// Audio handler that bridges media_kit player with OS media controls class MediaKitAudioHandler extends BaseAudioHandler with SeekHandler { - Player? _player; - VoidCallback? _onNext; - VoidCallback? _onPrevious; + Player _player; + final String plexServerUrl; + final String authToken; + // Getter for player + Player get player => _player; + + // Callback functions for episode navigation + Future Function()? onSkipToNext; + Future Function()? onSkipToPrevious; + + // Stream subscriptions StreamSubscription? _playingSubscription; StreamSubscription? _positionSubscription; - StreamSubscription? _completedSubscription; + StreamSubscription? _durationSubscription; + StreamSubscription? _bufferSubscription; + + // Queue management + final BehaviorSubject> _queueSubject = BehaviorSubject.seeded([]); MediaKitAudioHandler({ - required Player? player, - VoidCallback? onNext, - VoidCallback? onPrevious, - }) : _player = player, - _onNext = onNext, - _onPrevious = onPrevious { - if (_player != null) { - _initializeListeners(); + required Player player, + required this.plexServerUrl, + required this.authToken, + this.onSkipToNext, + this.onSkipToPrevious, + }) : _player = player { + _init(); + } + + void _init() { + // Map player state to playback state + _playingSubscription = _player.stream.playing.listen((isPlaying) { + _updatePlaybackState(); + }); + + _positionSubscription = _player.stream.position.listen((_) { + _updatePlaybackState(); + }); + + _durationSubscription = _player.stream.duration.listen((_) { + _updatePlaybackState(); + }); + + _bufferSubscription = _player.stream.buffer.listen((_) { + _updatePlaybackState(); + }); + + // Set initial state + _updatePlaybackState(); + } + + /// Update media item with current playing content + void updateCurrentMediaItem(PlexMetadata metadata) { + final artworkUrl = _getArtworkUrl(metadata); + + final newMediaItem = MediaItem( + id: metadata.ratingKey, + title: metadata.title, + album: _getAlbumName(metadata), + artist: _getArtistName(metadata), + duration: metadata.duration != null + ? Duration(milliseconds: metadata.duration!) + : null, + artUri: artworkUrl != null ? Uri.parse(artworkUrl) : null, + extras: { + 'ratingKey': metadata.ratingKey, + 'type': metadata.type, + }, + ); + + mediaItem.add(newMediaItem); + appLogger.d('Updated media item: ${metadata.title}'); + } + + /// Update navigation callbacks + void updateNavigationCallbacks({ + Future Function()? onNext, + Future Function()? onPrevious, + }) { + onSkipToNext = onNext; + onSkipToPrevious = onPrevious; + _updatePlaybackState(); + } + + /// Update the queue with episodes + void updateEpisodeQueue(List episodes, int currentIndex) { + final items = episodes.map((episode) { + final artworkUrl = _getArtworkUrl(episode); + return MediaItem( + id: episode.ratingKey, + title: episode.title, + album: _getAlbumName(episode), + artist: _getArtistName(episode), + duration: episode.duration != null + ? Duration(milliseconds: episode.duration!) + : null, + artUri: artworkUrl != null ? Uri.parse(artworkUrl) : null, + extras: { + 'ratingKey': episode.ratingKey, + 'type': episode.type, + }, + ); + }).toList(); + + _queueSubject.add(items); + queue.add(items); + + if (currentIndex >= 0 && currentIndex < items.length) { + mediaItem.add(items[currentIndex]); } + + appLogger.d('Updated queue with ${items.length} episodes, current index: $currentIndex'); } - void _initializeListeners() { - if (_player == null) return; + String? _getArtworkUrl(PlexMetadata metadata) { + String? thumbPath; - _playingSubscription = _player!.stream.playing.listen((_) { - _broadcastState(); - }); - - _positionSubscription = _player!.stream.position.listen((_) { - _broadcastState(); - }); - - _completedSubscription = _player!.stream.completed.listen((completed) { - if (completed) { - _broadcastState(); - } - }); - - _broadcastState(); - } - - /// Update the player reference and callbacks (for video changes) - Future updatePlayer({ - required Player? player, - VoidCallback? onNext, - VoidCallback? onPrevious, - }) async { - await _cleanupSubscriptions(); - - // Update player and callbacks - _player = player; - _onNext = onNext; - _onPrevious = onPrevious; - - // Set up new subscriptions if player is not null - if (_player != null) { - _initializeListeners(); + // For episodes, prefer show poster over episode thumbnail + if (metadata.type.toLowerCase() == 'episode') { + thumbPath = metadata.grandparentThumb ?? metadata.thumb; } else { - // No player, broadcast idle state - _broadcastIdleState(); + thumbPath = metadata.thumb; } + + if (thumbPath == null) return null; + + // Build full URL with authentication + return '$plexServerUrl$thumbPath?X-Plex-Token=$authToken'; } - /// Clean up current subscriptions - Future _cleanupSubscriptions() async { - await _playingSubscription?.cancel(); - await _positionSubscription?.cancel(); - await _completedSubscription?.cancel(); - _playingSubscription = null; - _positionSubscription = null; - _completedSubscription = null; - } - - void _broadcastState() { - if (_player == null || mediaItem.value == null) { - if (_player == null) { - _broadcastIdleState(); + String _getAlbumName(PlexMetadata metadata) { + // For episodes: "Show Name - Season X" + if (metadata.type.toLowerCase() == 'episode') { + final showName = metadata.grandparentTitle ?? ''; + final seasonNum = metadata.parentIndex; + if (seasonNum != null) { + return '$showName - Season $seasonNum'; } - return; + return showName; } - final playing = _player!.state.playing; - final position = _player!.state.position; - final duration = _player!.state.duration; - final rate = _player!.state.rate; + // For movies: studio or year + return metadata.studio ?? metadata.year?.toString() ?? ''; + } + + String _getArtistName(PlexMetadata metadata) { + // For episodes: show episode info + if (metadata.type.toLowerCase() == 'episode') { + final seasonNum = metadata.parentIndex; + final episodeNum = metadata.index; + if (seasonNum != null && episodeNum != null) { + return 'S${seasonNum.toString().padLeft(2, '0')}E${episodeNum.toString().padLeft(2, '0')}'; + } + } + + return ''; + } + + void _updatePlaybackState() { + final isPlaying = _player.state.playing; + final position = _player.state.position; + final bufferedPosition = _player.state.buffer; // Determine processing state AudioProcessingState processingState; - if (_player!.state.completed) { + if (_player.state.buffering) { + processingState = AudioProcessingState.buffering; + } else if (_player.state.completed) { processingState = AudioProcessingState.completed; - } else if (duration.inMilliseconds > 0) { - processingState = AudioProcessingState.ready; } else { - processingState = AudioProcessingState.loading; + processingState = AudioProcessingState.ready; } - // Build control list + // Build controls based on state and available navigation final controls = [ - if (_onPrevious != null) MediaControl.skipToPrevious, - playing ? MediaControl.pause : MediaControl.play, - MediaControl.stop, - if (_onNext != null) MediaControl.skipToNext, + if (onSkipToPrevious != null) MediaControl.skipToPrevious, + MediaControl.rewind, + if (isPlaying) MediaControl.pause else MediaControl.play, + MediaControl.fastForward, + if (onSkipToNext != null) MediaControl.skipToNext, ]; - final compactIndices = _calculateCompactActionIndices(controls); - playbackState.add(PlaybackState( controls: controls, systemActions: const { @@ -119,164 +197,118 @@ class MediaKitAudioHandler extends BaseAudioHandler with SeekHandler { MediaAction.seekForward, MediaAction.seekBackward, }, - androidCompactActionIndices: compactIndices, - playing: playing, - updatePosition: position, - speed: rate, + androidCompactActionIndices: [ + if (onSkipToPrevious != null) 0, + onSkipToPrevious != null ? 2 : 1, // Play/Pause button + if (onSkipToNext != null) + onSkipToPrevious != null ? 4 : 3, + ].where((i) => i < controls.length).toList(), processingState: processingState, + playing: isPlaying, + updatePosition: position, + bufferedPosition: bufferedPosition, + speed: 1.0, + queueIndex: 0, // TODO: Update based on actual queue position )); } - void _broadcastIdleState() { - playbackState.add(PlaybackState( - controls: [], - systemActions: const {}, - playing: false, - processingState: AudioProcessingState.idle, - )); - } - - /// Calculate compact action indices for Android notification - /// Returns indices for the most important controls to show in compact view - List _calculateCompactActionIndices(List controls) { - final indices = []; - - // Find indices of key controls - int? previousIndex; - int? playPauseIndex; - int? nextIndex; - - for (int i = 0; i < controls.length; i++) { - if (controls[i] == MediaControl.skipToPrevious) { - previousIndex = i; - } else if (controls[i] == MediaControl.play || - controls[i] == MediaControl.pause) { - playPauseIndex = i; - } else if (controls[i] == MediaControl.skipToNext) { - nextIndex = i; - } - } - - // Build compact indices: Previous (if exists), Play/Pause, Next (if exists) - if (previousIndex != null) indices.add(previousIndex); - if (playPauseIndex != null) indices.add(playPauseIndex); - if (nextIndex != null) indices.add(nextIndex); - - // Ensure we have at least the play/pause button - if (indices.isEmpty && playPauseIndex != null) { - indices.add(playPauseIndex); - } - - return indices; - } - - /// Update the media item shown in OS controls - void setMediaItemFromMetadata(PlexMetadata metadata, String? thumbnailUrl) { - final title = metadata.type.toLowerCase() == 'episode' - ? metadata.title - : metadata.title; - - final artist = metadata.type.toLowerCase() == 'episode' - ? metadata.grandparentTitle ?? metadata.year?.toString() - : metadata.year?.toString(); - - final album = metadata.type.toLowerCase() == 'episode' - ? 'S${metadata.parentIndex} · E${metadata.index} · ${metadata.parentTitle ?? ""}' - : metadata.studio; - - final duration = metadata.duration != null - ? Duration(milliseconds: metadata.duration!) - : Duration.zero; - - mediaItem.add(MediaItem( - id: metadata.ratingKey, - title: title, - artist: artist, - album: album, - duration: duration, - artUri: thumbnailUrl != null ? Uri.parse(thumbnailUrl) : null, - extras: { - 'ratingKey': metadata.ratingKey, - 'type': metadata.type, - }, - )); - - appLogger.i('Media item updated: $title${artist != null ? " - $artist" : ""}'); - } - - /// Update whether next/previous actions are available - void updateNavigationActions({bool? hasNext, bool? hasPrevious}) { - _broadcastState(); - } - - /// Force an immediate state update - /// Use this after playback starts to ensure notification appears - void forceStateUpdate() { - _broadcastState(); - } - - // BaseAudioHandler implementations @override Future play() async { - if (_player == null) return; - await _player!.play(); + appLogger.d('Audio handler: play'); + await _player.play(); } @override Future pause() async { - if (_player == null) return; - await _player!.pause(); - } - - @override - Future stop() async { - if (_player != null) { - await _player!.pause(); - } - - playbackState.add(PlaybackState( - controls: [], - systemActions: const {}, - playing: false, - processingState: AudioProcessingState.idle, - )); - - await super.stop(); + appLogger.d('Audio handler: pause'); + await _player.pause(); } @override Future seek(Duration position) async { - if (_player == null) return; - await _player!.seek(position); + appLogger.d('Audio handler: seek to ${position.inSeconds}s'); + await _player.seek(position); } @override - Future skipToNext() async { - _onNext?.call(); - } - - @override - Future skipToPrevious() async { - _onPrevious?.call(); + Future stop() async { + appLogger.d('Audio handler: stop'); + await _player.stop(); } @override Future fastForward() async { - if (_player == null) return; - final newPosition = _player!.state.position + const Duration(seconds: 15); - await _player!.seek(newPosition); + appLogger.d('Audio handler: fast forward'); + final newPosition = _player.state.position + const Duration(seconds: 10); + await _player.seek(newPosition); } @override Future rewind() async { - if (_player == null) return; - final newPosition = _player!.state.position - const Duration(seconds: 15); - await _player!.seek(newPosition > Duration.zero ? newPosition : Duration.zero); + appLogger.d('Audio handler: rewind'); + final newPosition = _player.state.position - const Duration(seconds: 10); + await _player.seek(newPosition > Duration.zero ? newPosition : Duration.zero); + } + + @override + Future skipToNext() async { + appLogger.d('Audio handler: skip to next'); + if (onSkipToNext != null) { + await onSkipToNext!(); + } + } + + @override + Future skipToPrevious() async { + appLogger.d('Audio handler: skip to previous'); + if (onSkipToPrevious != null) { + await onSkipToPrevious!(); + } + } + + /// Update the player reference when switching episodes + /// This ensures the handler stays in sync with the current player instance + Future updatePlayer(Player newPlayer) async { + appLogger.d('Audio handler: updating player reference'); + + // Cancel old subscriptions + await _playingSubscription?.cancel(); + await _positionSubscription?.cancel(); + await _durationSubscription?.cancel(); + await _bufferSubscription?.cancel(); + + // Update player reference + _player = newPlayer; + + // Create new subscriptions with the new player + _playingSubscription = _player.stream.playing.listen((isPlaying) { + _updatePlaybackState(); + }); + + _positionSubscription = _player.stream.position.listen((_) { + _updatePlaybackState(); + }); + + _durationSubscription = _player.stream.duration.listen((_) { + _updatePlaybackState(); + }); + + _bufferSubscription = _player.stream.buffer.listen((_) { + _updatePlaybackState(); + }); + + // Update playback state immediately + _updatePlaybackState(); + + appLogger.d('Audio handler: player reference updated successfully'); } Future dispose() async { - appLogger.d('Disposing MediaKitAudioHandler'); - await _cleanupSubscriptions(); - await stop(); + // Cancel subscriptions + await _playingSubscription?.cancel(); + await _positionSubscription?.cancel(); + await _durationSubscription?.cancel(); + await _bufferSubscription?.cancel(); + await _queueSubject.close(); } } diff --git a/lib/services/media_service_manager.dart b/lib/services/media_service_manager.dart deleted file mode 100644 index 15c1e2593..000000000 --- a/lib/services/media_service_manager.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:audio_service/audio_service.dart'; -import 'package:media_kit/media_kit.dart'; -import 'media_kit_audio_handler.dart'; -import '../utils/app_logger.dart'; - -/// Singleton manager for OS media controls integration -/// Manages a single AudioHandler instance for the entire app lifecycle -class MediaServiceManager { - static MediaServiceManager? _instance; - static MediaKitAudioHandler? _audioHandler; - static bool _isInitialized = false; - - MediaServiceManager._(); - - static MediaServiceManager get instance { - _instance ??= MediaServiceManager._(); - return _instance!; - } - - /// Initialize the audio service once at app startup - Future initialize() async { - if (_isInitialized) { - appLogger.w('MediaServiceManager already initialized'); - return; - } - - try { - appLogger.i('Initializing MediaServiceManager'); - - _audioHandler = await AudioService.init( - builder: () => MediaKitAudioHandler( - player: null, // Will be set when first video plays - onNext: null, - onPrevious: null, - ), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.plezy.app.channel.audio', - androidNotificationChannelName: 'Plezy Playback', - androidNotificationOngoing: false, - androidStopForegroundOnPause: true, - androidNotificationIcon: 'drawable/ic_stat_notification', - // Configure audio session for proper media playback - preloadArtwork: true, - ), - ); - - _isInitialized = true; - appLogger.i('MediaServiceManager initialized successfully'); - } catch (e, stackTrace) { - appLogger.e( - '❌ Failed to initialize MediaServiceManager', - error: e, - stackTrace: stackTrace, - ); - // Non-fatal, app can continue without OS media controls - } - } - - /// Update the audio handler with a new player and callbacks - Future updatePlayer({ - required Player? player, - VoidCallback? onNext, - VoidCallback? onPrevious, - }) async { - if (!_isInitialized || _audioHandler == null) { - appLogger.w('MediaServiceManager not initialized, cannot update player'); - return; - } - - try { - await _audioHandler!.updatePlayer( - player: player, - onNext: onNext, - onPrevious: onPrevious, - ); - } catch (e) { - appLogger.e('Failed to update player', error: e); - } - } - - /// Stop playback and clear OS controls - Future stop() async { - if (_audioHandler == null) return; - - try { - await _audioHandler!.stop(); - } catch (e) { - appLogger.e('Failed to stop audio service', error: e); - } - } - - /// Update the media item shown in OS controls - void updateMediaItem(dynamic metadata, String? thumbnailUrl) { - if (_audioHandler == null) return; - - try { - _audioHandler!.setMediaItemFromMetadata(metadata, thumbnailUrl); - } catch (e) { - appLogger.e('Failed to update media item', error: e); - } - } - - /// Update navigation actions availability - void updateNavigationActions({bool? hasNext, bool? hasPrevious}) { - _audioHandler?.updateNavigationActions( - hasNext: hasNext, - hasPrevious: hasPrevious, - ); - } - - /// Force an immediate state update to trigger notification - /// Call this after playback starts to ensure Android shows the notification - void forceStateUpdate() { - _audioHandler?.forceStateUpdate(); - } - - /// Check if the service is initialized - bool get isInitialized => _isInitialized; - - /// Get the audio handler (for advanced usage) - MediaKitAudioHandler? get audioHandler => _audioHandler; -} diff --git a/pubspec.lock b/pubspec.lock index 3dbbbdf21..9d457614d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -67,13 +67,13 @@ packages: source: hosted version: "0.1.4" audio_session: - dependency: transitive + dependency: "direct main" description: name: audio_session - sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.1.25" boolean_selector: dependency: transitive description: @@ -793,13 +793,13 @@ packages: source: hosted version: "4.1.0" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.27.7" safe_local_storage: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9712efb53..e4b463588 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,14 @@ dependencies: provider: ^6.1.2 hotkey_manager: ^0.2.3 flex_color_picker: ^3.6.0 - audio_service: ^0.18.18 qr_flutter: ^4.1.0 + audio_session: ^0.1.25 + rxdart: ^0.27.2 + audio_service: + git: + url: https://github.com/marckornberger/audio_service.git + ref: 4ca42f391183854095188a5a10fa6b59cd641e68 + path: audio_service dependency_overrides: media_kit: @@ -39,11 +45,6 @@ dependency_overrides: url: https://github.com/edde746/media-kit ref: b6de7e44148043bd26f2569e8062dc8bce4f11b9 path: libs/universal/media_kit_libs_video - audio_service: - git: - url: https://github.com/marckornberger/audio_service.git - ref: 4ca42f391183854095188a5a10fa6b59cd641e68 - path: audio_service dev_dependencies: flutter_test: From 260681aa2424e7bc5bcbab541eb4621f5101dcd8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 8 Nov 2025 10:10:58 +0100 Subject: [PATCH 09/25] chore: bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e4b463588..fd1382aa1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: plezy description: "A beautiful Plex client for Flutter" publish_to: "none" -version: 1.5.1+13 +version: 1.6.0+14 environment: sdk: ^3.8.1 From 2d706956da67a35ac3c8f1874a952806af3bd772 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:53:54 +0100 Subject: [PATCH 10/25] feat: re-do os media controls --- android/app/src/main/AndroidManifest.xml | 20 +- .../flutter_application_1/MainActivity.kt | 4 +- ios/Podfile.lock | 19 +- ios/Runner/AppDelegate.swift | 13 + lib/screens/video_player_screen.dart | 195 ++++++++--- lib/services/audio_service_manager.dart | 186 ----------- lib/services/media_kit_audio_handler.dart | 314 ------------------ macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- macos/Podfile.lock | 19 +- pubspec.lock | 54 +-- pubspec.yaml | 9 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 189 insertions(+), 654 deletions(-) delete mode 100644 lib/services/audio_service_manager.dart delete mode 100644 lib/services/media_kit_audio_handler.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0dfda6f04..13b13acc2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,8 +3,7 @@ - - + @@ -45,23 +44,6 @@ - - - - - - - - - - - - - -