From b6299984168394c6db85a835afaa5e87ddedca65 Mon Sep 17 00:00:00 2001 From: Pato05 Date: Tue, 17 Oct 2023 16:03:39 +0200 Subject: [PATCH] right click support add app-icon on desktop --- lib/api/player.dart | 5 + lib/page_routes/fade.dart | 3 + lib/ui/cached_image.dart | 5 +- lib/ui/details_screens.dart | 194 ++++++++------- lib/ui/home_screen.dart | 4 +- lib/ui/library.dart | 1 + lib/ui/menu.dart | 326 +++++++++++++++----------- lib/ui/player_bar.dart | 16 +- lib/ui/player_screen.dart | 11 +- lib/ui/queue_screen.dart | 5 +- lib/ui/search.dart | 19 +- lib/ui/tiles.dart | 142 ++++++----- linux/CMakeLists.txt | 3 + linux/app_icon.ico | Bin 0 -> 270622 bytes linux/my_application.cc | 66 ++++-- pubspec.lock | 28 +-- windows/runner/main.cpp | 12 +- windows/runner/resources/app_icon.ico | Bin 33772 -> 270622 bytes 18 files changed, 464 insertions(+), 376 deletions(-) create mode 100644 linux/app_icon.ico diff --git a/lib/api/player.dart b/lib/api/player.dart index e20a105..e091d40 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -12,6 +12,7 @@ import 'package:freezer/ui/android_auto.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:just_audio/just_audio.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -432,6 +433,10 @@ class AudioPlayerTask extends BaseAudioHandler { } Future _init(AudioPlayerTaskInitArguments initArgs) async { + // Linux/Windows specific options + JustAudioMediaKit.title = 'Freezer'; + JustAudioMediaKit.protocolWhitelist = const ['http']; + _deezerAPI = initArgs.deezerAPI; _androidAuto = AndroidAuto(deezerAPI: _deezerAPI); _shouldLogTracks = initArgs.logListen; diff --git a/lib/page_routes/fade.dart b/lib/page_routes/fade.dart index 85a6a04..54a060d 100644 --- a/lib/page_routes/fade.dart +++ b/lib/page_routes/fade.dart @@ -7,6 +7,8 @@ class FadePageRoute extends BasicPageRoute { final bool barrierDismissible; @override final Color? barrierColor; + @override + final bool opaque; final WidgetBuilder builder; final bool blur; @@ -18,6 +20,7 @@ class FadePageRoute extends BasicPageRoute { super.settings, this.barrierColor, this.barrierDismissible = false, + this.opaque = true, }); @override diff --git a/lib/ui/cached_image.dart b/lib/ui/cached_image.dart index 9789f9e..bdf8f61 100644 --- a/lib/ui/cached_image.dart +++ b/lib/ui/cached_image.dart @@ -147,7 +147,8 @@ class ZoomableImage extends StatelessWidget { Navigator.of(context).push(FadePageRoute( builder: (context) => ZoomableImageRoute(imageUrl: url, heroKey: _key), - barrierDismissible: true)); + barrierDismissible: true, + opaque: false)); }, ); } @@ -163,7 +164,7 @@ class ZoomableImageRoute extends StatefulWidget { } class _ZoomableImageRouteState extends State { - bool photoViewOpened = false; + bool photoViewOpened = true; final controller = PhotoViewController(); final _focusNode = FocusScopeNode(); diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index ddadfd0..3c89961 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -73,51 +73,48 @@ class _AlbumDetailsState extends State { : ListView( children: [ //Album art, title, artists - Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8.0), - ConstrainedBox( - constraints: BoxConstraints.loose( - MediaQuery.of(context).size / 3), - child: ZoomableImage( - url: album!.art!.full, - rounded: true, - ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8.0), + ConstrainedBox( + constraints: BoxConstraints.loose( + MediaQuery.of(context).size / 2.5), + child: ZoomableImage( + url: album!.art!.full, + rounded: true, ), - const SizedBox(height: 8.0), + ), + const SizedBox(height: 8.0), + Text( + album!.title!, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + Text( + album!.artistString, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context).primaryColor), + ), + const SizedBox(height: 4.0), + if (album!.releaseDate != null && + album!.releaseDate!.length >= 4) Text( - album!.title!, + album!.releaseDate!, textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.bold), - ), - Text( - album!.artistString, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryColor), + fontSize: 12.0, + color: Theme.of(context).disabledColor), ), - const SizedBox(height: 4.0), - if (album!.releaseDate != null && - album!.releaseDate!.length >= 4) - Text( - album!.releaseDate!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.0, - color: Theme.of(context).disabledColor), - ), - const SizedBox(height: 8.0), - ], - ), + const SizedBox(height: 8.0), + ], ), const FreezerDivider(), //Details @@ -355,14 +352,15 @@ class _ArtistDetailsState extends State { const SizedBox(height: 4.0), Padding( padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: MediaQuery.of(context).size.height / 3, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height / 3), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Flexible( child: ZoomableImage( - url: widget.artist.picture!.full, + url: artist.picture!.full, rounded: true, ), ), @@ -1090,12 +1088,8 @@ class _PlaylistDetailsState extends State { }, onSecondary: (details) { MenuSheet m = MenuSheet(context); m.defaultTrackMenu(t, details: details, options: [ - (playlist!.user!.id == deezerAPI.userId) - ? m.removeFromPlaylist(t, playlist) - : const SizedBox( - width: 0, - height: 0, - ) + if (playlist!.user!.id == deezerAPI.userId) + m.removeFromPlaylist(t, playlist) ]); }); }), @@ -1128,9 +1122,11 @@ class _MakePlaylistOfflineState extends State { @override void initState() { downloadManager.checkOffline(playlist: widget.playlist).then((v) { - setState(() { - _offline = v; - }); + if (mounted) { + setState(() { + _offline = v; + }); + } }); super.initState(); } @@ -1221,55 +1217,55 @@ class _ShowScreenState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - CachedImage( - url: _show!.art!.full, - rounded: true, - width: MediaQuery.of(context).size.width / 2 - 16, - ), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 16, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Text(_show!.name!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.bold)), - Container(height: 8.0), - Text( - _show!.description!, - maxLines: 6, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16.0), - ) - ], + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height / 3), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: AspectRatio( + aspectRatio: 1.0, + child: CachedImage( + url: _show!.art!.full, + rounded: true, + ), + ), ), - ) - ], + SizedBox( + width: min(MediaQuery.of(context).size.width / 16, 60.0)), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_show!.name!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold)), + const SizedBox(height: 16.0), + Text( + _show!.description!, + maxLines: 6, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16.0), + ) + ], + ), + ) + ], + ), ), ), - Container(height: 4.0), + const SizedBox(height: 4.0), const FreezerDivider(), //Error if (_error) const ErrorScreen(), //Loading - if (_loading) - const Padding( - padding: EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], - ), - ), + if (_loading) const Center(child: CircularProgressIndicator()), //Data if (!_loading && !_error) @@ -1277,16 +1273,10 @@ class _ShowScreenState extends State { ShowEpisode e = _episodes![i]; return ShowEpisodeTile( e, - trailing: IconButton( - icon: Icon( - Icons.more_vert, - semanticLabel: "Options".i18n, - ), - onPressed: () { - MenuSheet m = MenuSheet(context); - m.defaultShowEpisodeMenu(_show!, e); - }, - ), + onSecondary: (details) { + MenuSheet m = MenuSheet(context); + m.defaultShowEpisodeMenu(_show!, e, details: details); + }, onTap: () async { await playerHelper.playShowEpisode(_show!, _episodes!, index: i); diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index da3187a..4ad1170 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -340,9 +340,9 @@ class HomePageItemWidget extends StatelessWidget { Navigator.of(context) .pushRoute(builder: (context) => PlaylistDetails(item.value)); }, - onHold: () { + onSecondary: (details) { MenuSheet m = MenuSheet(context); - m.defaultPlaylistMenu(item.value); + m.defaultPlaylistMenu(item.value, details: details); }, ); case HomePageItemType.CHANNEL: diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 5dcad32..89f21d0 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -1217,6 +1217,7 @@ class _HistoryScreenState extends State { MenuSheet m = MenuSheet(context); m.defaultTrackMenu(t, details: details); }, + checkTrackOffline: false, ); }, )), diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index b709978..5683d38 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ffi'; import 'package:freezer/main.dart'; import 'package:freezer/ui/player_bar.dart'; @@ -93,17 +92,57 @@ class SliverTrackPersistentHeader extends SliverPersistentHeaderDelegate { } } +class MenuSheetOption { + final Widget label; + final Widget? icon; + final VoidCallback onTap; + + const MenuSheetOption( + this.label, { + required this.onTap, + this.icon, + }); +} + class MenuSheet { BuildContext context; Function? navigateCallback; MenuSheet(this.context, {this.navigateCallback}); + void _showContextMenu(List options, + {required TapDownDetails details}) { + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final actualPosition = overlay.globalToLocal(details.globalPosition); + showMenu( + elevation: 4.0, + context: context, + constraints: const BoxConstraints(maxWidth: 300), + position: + RelativeRect.fromSize(actualPosition & Size.zero, overlay.size), + items: options + .map((option) => PopupMenuItem( + onTap: option.onTap, + child: option.icon == null + ? option.label + : Row(mainAxisSize: MainAxisSize.min, children: [ + option.icon!, + const SizedBox(width: 8.0), + option.label, + ]))) + .toList(growable: false)); + } + //=================== // DEFAULT //=================== - void show(List options) { + void show(List options, {TapDownDetails? details}) { + if (details != null) { + _showContextMenu(options, details: details); + return; + } + showModalBottomSheet( isScrollControlled: false, // true, context: context, @@ -117,7 +156,14 @@ class MenuSheet { : 350, ), child: SingleChildScrollView( - child: Column(children: options), + child: Column( + children: options + .map((option) => ListTile( + title: option.label, + leading: option.icon, + onTap: option.onTap, + )) + .toList(growable: false)), ), ), ); @@ -128,7 +174,13 @@ class MenuSheet { // TRACK //=================== - void showWithTrack(Track track, List options) { + void showWithTrack(Track track, List options, + {TapDownDetails? details}) { + if (details != null) { + _showContextMenu(options, details: details); + return; + } + showModalBottomSheet( backgroundColor: Colors.transparent, context: context, @@ -154,7 +206,14 @@ class MenuSheet { pinned: true, delegate: SliverTrackPersistentHeader(track, extent: 128.0 + 16.0 + 16.0)), - SliverList(delegate: SliverChildListDelegate.fixed(options)), + SliverList( + delegate: SliverChildListDelegate.fixed(options + .map((option) => ListTile( + title: option.label, + leading: option.icon, + onTap: option.onTap, + )) + .toList(growable: false))), ], ), ), @@ -165,53 +224,53 @@ class MenuSheet { //Default track options void defaultTrackMenu( Track track, { - List options = const [], + List options = const [], Function? onRemove, TapDownDetails? details, }) { - showWithTrack(track, [ - addToQueueNext(track), - addToQueue(track), - (cache.checkTrackFavorite(track)) - ? removeFavoriteTrack(track, onUpdate: onRemove) - : addTrackFavorite(track), - addToPlaylist(track), - downloadTrack(track), - offlineTrack(track), - shareTile('track', track.id), - playMix(track), - showAlbum(track.album!), - ...List.generate( - track.artists!.length, (i) => showArtist(track.artists![i])), - ...options - ]); + showWithTrack( + track, + [ + addToQueueNext(track), + addToQueue(track), + (cache.checkTrackFavorite(track)) + ? removeFavoriteTrack(track, onUpdate: onRemove) + : addTrackFavorite(track), + addToPlaylist(track), + downloadTrack(track), + offlineTrack(track), + shareTile('track', track.id), + playMix(track), + showAlbum(track.album!), + ...List.generate( + track.artists!.length, (i) => showArtist(track.artists![i])), + ...options + ], + details: details); } //=================== // TRACK OPTIONS //=================== - Widget addToQueueNext(Track t) => ListTile( - title: Text('Play next'.i18n), - leading: const Icon(Icons.playlist_play), - onTap: () async { + MenuSheetOption addToQueueNext(Track t) => + MenuSheetOption(Text('Play next'.i18n), + icon: const Icon(Icons.playlist_play), onTap: () async { //-1 = next await audioHandler.insertQueueItem(-1, await t.toMediaItem()); _close(); }); - Widget addToQueue(Track t) => ListTile( - title: Text('Add to queue'.i18n), - leading: const Icon(Icons.playlist_add), - onTap: () async { + MenuSheetOption addToQueue(Track t) => + MenuSheetOption(Text('Add to queue'.i18n), + icon: const Icon(Icons.playlist_add), onTap: () async { await audioHandler.addQueueItem(await t.toMediaItem()); _close(); }); - Widget addTrackFavorite(Track t) => ListTile( - title: Text('Add track to favorites'.i18n), - leading: const Icon(Icons.favorite), - onTap: () async { + MenuSheetOption addTrackFavorite(Track t) => + MenuSheetOption(Text('Add track to favorites'.i18n), + icon: const Icon(Icons.favorite), onTap: () async { await deezerAPI.addFavoriteTrack(t.id); //Make track offline, if favorites are offline Playlist p = Playlist(id: deezerAPI.favoritesPlaylistId!); @@ -225,9 +284,9 @@ class MenuSheet { _close(); }); - Widget downloadTrack(Track t) => ListTile( - title: Text('Download'.i18n), - leading: const Icon(Icons.file_download), + MenuSheetOption downloadTrack(Track t) => MenuSheetOption( + Text('Download'.i18n), + icon: const Icon(Icons.file_download), onTap: () async { if (await downloadManager.addOfflineTrack(t, private: false, context: context, isSingleton: true) != @@ -236,9 +295,9 @@ class MenuSheet { }, ); - Widget addToPlaylist(Track t) => ListTile( - title: Text('Add to playlist'.i18n), - leading: const Icon(Icons.playlist_add), + MenuSheetOption addToPlaylist(Track t) => MenuSheetOption( + Text('Add to playlist'.i18n), + icon: const Icon(Icons.playlist_add), onTap: () async { //Show dialog to pick playlist await showDialog( @@ -260,9 +319,9 @@ class MenuSheet { }, ); - Widget removeFromPlaylist(Track t, Playlist? p) => ListTile( - title: Text('Remove from playlist'.i18n), - leading: const Icon(Icons.delete), + MenuSheetOption removeFromPlaylist(Track t, Playlist? p) => MenuSheetOption( + Text('Remove from playlist'.i18n), + icon: const Icon(Icons.delete), onTap: () async { await deezerAPI.removeFromPlaylist(t.id, p!.id); ScaffoldMessenger.of(context) @@ -271,9 +330,9 @@ class MenuSheet { }, ); - Widget removeFavoriteTrack(Track t, {onUpdate}) => ListTile( - title: Text('Remove favorite'.i18n), - leading: const Icon(Icons.delete), + MenuSheetOption removeFavoriteTrack(Track t, {onUpdate}) => MenuSheetOption( + Text('Remove favorite'.i18n), + icon: const Icon(Icons.delete), onTap: () async { await deezerAPI.removeFavorite(t.id); //Check if favorites playlist is offline, update it @@ -282,9 +341,7 @@ class MenuSheet { await downloadManager.addOfflinePlaylist(p); } //Remove from cache - if (cache.libraryTracks != null) { - cache.libraryTracks!.removeWhere((i) => i == t.id); - } + cache.libraryTracks.removeWhere((i) => i == t.id); ScaffoldMessenger.of(context) .snack('Track removed from library'.i18n); if (onUpdate != null) onUpdate(); @@ -293,13 +350,13 @@ class MenuSheet { ); //Redirect to artist page (ie from track) - Widget showArtist(Artist a) => ListTile( - title: Text( + MenuSheetOption showArtist(Artist a) => MenuSheetOption( + Text( '${'Go to'.i18n} ${a.name}', maxLines: 1, overflow: TextOverflow.ellipsis, ), - leading: const Icon(Icons.recent_actors), + icon: const Icon(Icons.recent_actors), onTap: () { _close(); navigatorKey.currentState! @@ -311,13 +368,13 @@ class MenuSheet { }, ); - Widget showAlbum(Album a) => ListTile( - title: Text( + MenuSheetOption showAlbum(Album a) => MenuSheetOption( + Text( '${'Go to'.i18n} ${a.title}', maxLines: 1, overflow: TextOverflow.ellipsis, ), - leading: const Icon(Icons.album), + icon: const Icon(Icons.album), onTap: () { _close(); navigatorKey.currentState! @@ -329,36 +386,33 @@ class MenuSheet { }, ); - Widget playMix(Track track) => ListTile( - title: Text('Play mix'.i18n), - leading: const Icon(Icons.online_prediction), + MenuSheetOption playMix(Track track) => MenuSheetOption( + Text('Play mix'.i18n), + icon: const Icon(Icons.online_prediction), onTap: () async { playerHelper.playMix(track.id, track.title!); _close(); }, ); - Widget offlineTrack(Track track) => FutureBuilder( - future: downloadManager.checkOffline(track: track), - builder: (context, snapshot) { - bool isOffline = snapshot.data ?? track.offline ?? false; - return ListTile( - title: Text(isOffline ? 'Remove offline'.i18n : 'Offline'.i18n), - leading: const Icon(Icons.offline_pin), - onTap: () async { - if (isOffline) { - await downloadManager.removeOfflineTracks([track]); - ScaffoldMessenger.of(context) - .snack("Track removed from offline!".i18n); - } else { - await downloadManager.addOfflineTrack(track, - private: true, context: context); - } - _close(); - }, - ); - }, - ); + MenuSheetOption offlineTrack(Track track) => MenuSheetOption( + FutureBuilder( + future: downloadManager.checkOffline(track: track), + builder: (context, snapshot) { + bool isOffline = snapshot.data ?? track.offline ?? false; + return Text(isOffline ? 'Remove offline'.i18n : 'Offline'.i18n); + }), + icon: const Icon(Icons.offline_pin), onTap: () async { + if (await downloadManager.checkOffline(track: track)) { + await downloadManager.removeOfflineTracks([track]); + ScaffoldMessenger.of(context) + .snack("Track removed from offline!".i18n); + } else { + await downloadManager.addOfflineTrack(track, + private: true, context: context); + } + _close(); + }); //=================== // ALBUM @@ -366,7 +420,7 @@ class MenuSheet { //Default album options void defaultAlbumMenu(Album album, - {List options = const [], + {List options = const [], Function? onRemove, TapDownDetails? details}) { show([ @@ -384,19 +438,18 @@ class MenuSheet { // ALBUM OPTIONS //=================== - Widget downloadAlbum(Album a) => ListTile( - title: Text('Download'.i18n), - leading: const Icon(Icons.file_download), - onTap: () async { + MenuSheetOption downloadAlbum(Album a) => + MenuSheetOption(Text('Download'.i18n), + icon: const Icon(Icons.file_download), onTap: () async { _close(); if (await downloadManager.addOfflineAlbum(a, private: false, context: context) != false) showDownloadStartedToast(); }); - Widget offlineAlbum(Album a) => ListTile( - title: Text('Make offline'.i18n), - leading: const Icon(Icons.offline_pin), + MenuSheetOption offlineAlbum(Album a) => MenuSheetOption( + Text('Make offline'.i18n), + icon: const Icon(Icons.offline_pin), onTap: () async { await deezerAPI.addFavoriteAlbum(a.id); await downloadManager.addOfflineAlbum(a, private: true); @@ -405,9 +458,9 @@ class MenuSheet { }, ); - Widget libraryAlbum(Album a) => ListTile( - title: Text('Add to library'.i18n), - leading: const Icon(Icons.library_music), + MenuSheetOption libraryAlbum(Album a) => MenuSheetOption( + Text('Add to library'.i18n), + icon: const Icon(Icons.library_music), onTap: () async { await deezerAPI.addFavoriteAlbum(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); @@ -416,9 +469,9 @@ class MenuSheet { ); //Remove album from favorites - Widget removeAlbum(Album a, {Function? onRemove}) => ListTile( - title: Text('Remove album'.i18n), - leading: const Icon(Icons.delete), + MenuSheetOption removeAlbum(Album a, {Function? onRemove}) => MenuSheetOption( + Text('Remove album'.i18n), + icon: const Icon(Icons.delete), onTap: () async { await deezerAPI.removeAlbum(a.id); await downloadManager.removeOfflineAlbum(a.id); @@ -433,10 +486,10 @@ class MenuSheet { //=================== void defaultArtistMenu(Artist artist, - {List options = const [], + {List options = const [], Function? onRemove, TapDownDetails? details}) { - show([ + show(details: details, [ artist.library! ? removeArtist(artist, onRemove: onRemove) : favoriteArtist(artist), @@ -449,9 +502,10 @@ class MenuSheet { // ARTIST OPTIONS //=================== - Widget removeArtist(Artist a, {Function? onRemove}) => ListTile( - title: Text('Remove from favorites'.i18n), - leading: const Icon(Icons.delete), + MenuSheetOption removeArtist(Artist a, {Function? onRemove}) => + MenuSheetOption( + Text('Remove from favorites'.i18n), + icon: const Icon(Icons.delete), onTap: () async { await deezerAPI.removeArtist(a.id); ScaffoldMessenger.of(context) @@ -461,9 +515,9 @@ class MenuSheet { }, ); - Widget favoriteArtist(Artist a) => ListTile( - title: Text('Add to favorites'.i18n), - leading: const Icon(Icons.favorite), + MenuSheetOption favoriteArtist(Artist a) => MenuSheetOption( + Text('Add to favorites'.i18n), + icon: const Icon(Icons.favorite), onTap: () async { await deezerAPI.addFavoriteArtist(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); @@ -476,11 +530,11 @@ class MenuSheet { //=================== void defaultPlaylistMenu(Playlist playlist, - {List options = const [], + {List options = const [], Function? onRemove, Function? onUpdate, TapDownDetails? details}) { - show([ + show(details: details, [ if (playlist.library != null) playlist.library! ? removePlaylistLibrary(playlist, onRemove: onRemove) @@ -498,9 +552,10 @@ class MenuSheet { // PLAYLIST OPTIONS //=================== - Widget removePlaylistLibrary(Playlist p, {Function? onRemove}) => ListTile( - title: Text('Remove from library'.i18n), - leading: const Icon(Icons.delete), + MenuSheetOption removePlaylistLibrary(Playlist p, {Function? onRemove}) => + MenuSheetOption( + Text('Remove from library'.i18n), + icon: const Icon(Icons.delete), onTap: () async { if (p.user!.id!.trim() == deezerAPI.userId) { //Delete playlist if own @@ -515,9 +570,9 @@ class MenuSheet { }, ); - Widget addPlaylistLibrary(Playlist p) => ListTile( - title: Text('Add playlist to library'.i18n), - leading: const Icon(Icons.favorite), + MenuSheetOption addPlaylistLibrary(Playlist p) => MenuSheetOption( + Text('Add playlist to library'.i18n), + icon: const Icon(Icons.favorite), onTap: () async { await deezerAPI.addPlaylist(p.id); ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n); @@ -525,9 +580,9 @@ class MenuSheet { }, ); - Widget addPlaylistOffline(Playlist p) => ListTile( - title: Text('Make playlist offline'.i18n), - leading: const Icon(Icons.offline_pin), + MenuSheetOption addPlaylistOffline(Playlist p) => MenuSheetOption( + Text('Make playlist offline'.i18n), + icon: const Icon(Icons.offline_pin), onTap: () async { //Add to library await deezerAPI.addPlaylist(p.id); @@ -537,9 +592,9 @@ class MenuSheet { }, ); - Widget downloadPlaylist(Playlist p) => ListTile( - title: Text('Download playlist'.i18n), - leading: const Icon(Icons.file_download), + MenuSheetOption downloadPlaylist(Playlist p) => MenuSheetOption( + Text('Download playlist'.i18n), + icon: const Icon(Icons.file_download), onTap: () async { _close(); if (await downloadManager.addOfflinePlaylist(p, @@ -548,9 +603,10 @@ class MenuSheet { }, ); - Widget editPlaylist(Playlist p, {Function? onUpdate}) => ListTile( - title: Text('Edit playlist'.i18n), - leading: const Icon(Icons.edit), + MenuSheetOption editPlaylist(Playlist p, {Function? onUpdate}) => + MenuSheetOption( + Text('Edit playlist'.i18n), + icon: const Icon(Icons.edit), onTap: () async { await showDialog( context: context, @@ -565,8 +621,8 @@ class MenuSheet { //=================== defaultShowEpisodeMenu(Show s, ShowEpisode e, - {List options = const []}) { - show([ + {List options = const [], TapDownDetails? details}) { + show(details: details, [ shareTile('episode', e.id), shareShow(s.id), downloadExternalEpisode(e), @@ -574,18 +630,18 @@ class MenuSheet { ]); } - Widget shareShow(String? id) => ListTile( - title: Text('Share show'.i18n), - leading: const Icon(Icons.share), + MenuSheetOption shareShow(String? id) => MenuSheetOption( + Text('Share show'.i18n), + icon: const Icon(Icons.share), onTap: () async { Share.share('https://deezer.com/show/$id'); }, ); //Open direct download link in browser - Widget downloadExternalEpisode(ShowEpisode e) => ListTile( - title: Text('Download externally'.i18n), - leading: const Icon(Icons.file_download), + MenuSheetOption downloadExternalEpisode(ShowEpisode e) => MenuSheetOption( + Text('Download externally'.i18n), + icon: const Icon(Icons.file_download), onTap: () async { launchUrl(Uri.parse(e.url!)); }, @@ -608,17 +664,17 @@ class MenuSheet { }); } - Widget shareTile(String type, String? id) => ListTile( - title: Text('Share'.i18n), - leading: const Icon(Icons.share), + MenuSheetOption shareTile(String type, String? id) => MenuSheetOption( + Text('Share'.i18n), + icon: const Icon(Icons.share), onTap: () async { Share.share('https://deezer.com/$type/$id'); }, ); - Widget sleepTimer() => ListTile( - title: Text('Sleep timer'.i18n), - leading: const Icon(Icons.access_time), + MenuSheetOption sleepTimer() => MenuSheetOption( + Text('Sleep timer'.i18n), + icon: const Icon(Icons.access_time), onTap: () async { showDialog( context: context, @@ -628,9 +684,9 @@ class MenuSheet { }, ); - Widget wakelock() => ListTile( - title: Text('Keep the screen on'.i18n), - leading: const Icon(Icons.screen_lock_portrait), + MenuSheetOption wakelock() => MenuSheetOption( + Text('Keep the screen on'.i18n), + icon: const Icon(Icons.screen_lock_portrait), onTap: () async { _close(); //Enable diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 489e920..7fd570d 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -237,7 +237,15 @@ class PlayerBar extends StatelessWidget { if (snapshot.data == null) { return Material( child: ListTile( - leading: Image.asset('assets/cover_thumb.jpg'), + dense: true, + visualDensity: VisualDensity.standard, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 6.0), + leading: Image.asset( + 'assets/cover_thumb.jpg', + width: 48.0, + height: 48.0, + ), title: Text('Nothing is currently playing'.i18n), ), ); @@ -254,10 +262,12 @@ class PlayerBar extends StatelessWidget { : image; return Material( child: ListTile( + dense: true, tileColor: _backgroundColor, focusNode: focusNode, - contentPadding: - const EdgeInsets.symmetric(horizontal: 8.0), + visualDensity: VisualDensity.standard, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 0.0), onTap: onTap, leading: AnimatedSwitcher( key: const ValueKey('player_bar_art_switcher'), diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index ff29b1e..02a1960 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_import + import 'dart:ui'; import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; @@ -691,16 +693,14 @@ class _FavoriteButtonState extends State { icon: libraryIcon, iconSize: widget.size, onPressed: () async { - cache.libraryTracks ??= []; - if (cache.checkTrackFavorite(Track.fromMediaItem(mediaItem))) { //Remove from library - setState(() => cache.libraryTracks!.remove(mediaItem.id)); + setState(() => cache.libraryTracks.remove(mediaItem.id)); await deezerAPI.removeFavorite(mediaItem.id); await cache.save(); } else { //Add - setState(() => cache.libraryTracks!.add(mediaItem.id)); + setState(() => cache.libraryTracks.add(mediaItem.id)); await deezerAPI.addFavoriteTrack(mediaItem.id); await cache.save(); } @@ -840,6 +840,7 @@ class _BigAlbumArtState extends State { context, FadePageRoute( barrierDismissible: true, + opaque: false, builder: (context) { final mediaItem = audioHandler.mediaItem.value!; return ZoomableImageRoute( @@ -867,7 +868,7 @@ class _BigAlbumArtState extends State { onHorizontalDragDown: (_) => _userScroll = true, // delayed a bit, so to make sure that the page view updated. onHorizontalDragEnd: (_) => Future.delayed( - const Duration(milliseconds: 500), () => _userScroll = false), + const Duration(milliseconds: 100), () => _userScroll = false), child: StreamBuilder>( stream: audioHandler.queue, initialData: audioHandler.queue.valueOrNull, diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index 2cadd22..c5da696 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -168,8 +168,9 @@ class _QueueListWidgetState extends State { } }); }, - onSecondary: (_) => MenuSheet(context) - .defaultTrackMenu(Track.fromMediaItem(mediaItem)), + onSecondary: (details) => MenuSheet(context).defaultTrackMenu( + Track.fromMediaItem(mediaItem), + details: details), checkTrackOffline: false, ), ), diff --git a/lib/ui/search.dart b/lib/ui/search.dart index 0c9384e..57b2d44 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -550,11 +550,17 @@ class SearchResultsScreen extends StatelessWidget { ), ), const SizedBox(height: 4.0), - SingleChildScrollView( + SizedBox( + height: 136.0, + child: ListView.builder( + primary: false, scrollDirection: Axis.horizontal, - child: Row(children: [ - for (final artist in results.artists!) - ArtistTile( + itemCount: results.artists!.length, + itemBuilder: (context, index) { + final artist = results.artists![index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ArtistTile( artist, onTap: () { cache.addToSearchHistory(artist); @@ -566,7 +572,10 @@ class SearchResultsScreen extends StatelessWidget { m.defaultArtistMenu(artist, details: details); }, ), - ])), + ); + }, + ), + ), const FreezerDivider() ], if (results.playlists != null && diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 56bbaee..e6555a0 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -14,21 +14,6 @@ import 'dart:async'; typedef SecondaryTapCallback = void Function(TapDownDetails?); -class WrapSecondaryAction extends StatelessWidget { - final SecondaryTapCallback? onSecondaryTapDown; - final Widget child; - const WrapSecondaryAction( - {super.key, this.onSecondaryTapDown, required this.child}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onSecondaryTapDown: onSecondaryTapDown, - child: child, - ); - } -} - VoidCallback? normalizeSecondary(SecondaryTapCallback? callback) { if (callback == null) return null; @@ -75,7 +60,7 @@ class TrackTile extends StatelessWidget { title: track.title!, artist: track.artistString, artUri: track.albumArt!.thumb, - explicit: track.explicit!, + explicit: track.explicit ?? false, durationString: track.durationString, onSecondary: onSecondary, onTap: onTap, @@ -91,7 +76,7 @@ class TrackTile extends StatelessWidget { TrackTile( trackId: mediaItem.id, title: mediaItem.title, - artist: mediaItem.artist!, + artist: mediaItem.artist ?? '', artUri: mediaItem.extras!['thumb'], explicit: false, durationString: Track.durationAsString(mediaItem.duration!), @@ -103,7 +88,7 @@ class TrackTile extends StatelessWidget { @override Widget build(BuildContext context) { - return WrapSecondaryAction( + return GestureDetector( onSecondaryTapDown: onSecondary, child: ListTile( title: StreamBuilder( @@ -188,7 +173,7 @@ class AlbumTile extends StatelessWidget { @override Widget build(BuildContext context) { - return WrapSecondaryAction( + return GestureDetector( onSecondaryTapDown: onSecondary, child: ListTile( title: Text( @@ -228,13 +213,13 @@ class ArtistTile extends StatelessWidget { onLongPress: normalizeSecondary(onSecondary), onSecondaryTapDown: onSecondary, child: Column(mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 4), + const SizedBox(height: 4.0), CachedImage( url: artist!.picture!.thumb, circular: true, width: 100, ), - const SizedBox(height: 8), + const SizedBox(height: 8.0), Text( artist!.name!, maxLines: 1, @@ -269,7 +254,7 @@ class PlaylistTile extends StatelessWidget { @override Widget build(BuildContext context) { - return WrapSecondaryAction( + return GestureDetector( onSecondaryTapDown: onSecondary, child: ListTile( title: Text( @@ -323,59 +308,63 @@ class ArtistHorizontalTile extends StatelessWidget { class PlaylistCardTile extends StatelessWidget { final Playlist? playlist; - final Function? onTap; - final Function? onHold; - const PlaylistCardTile(this.playlist, {super.key, this.onTap, this.onHold}); + final VoidCallback? onTap; + final SecondaryTapCallback? onSecondary; + const PlaylistCardTile(this.playlist, + {super.key, this.onTap, this.onSecondary}); @override Widget build(BuildContext context) { - return SizedBox( - height: 180.0, - child: InkWell( - onTap: onTap as void Function()?, - onLongPress: onHold as void Function()?, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - CachedImage( - url: playlist!.image!.thumb, - width: 128.0, - height: 128.0, - rounded: true, - ), - Positioned( - bottom: 8.0, - left: 8.0, - child: PlayItemButton( - onTap: () async { - final Playlist fullPlaylist = - await deezerAPI.fullPlaylist(playlist!.id); - await playerHelper.playFromPlaylist(fullPlaylist); - }, - )) - ], + return GestureDetector( + onSecondaryTapDown: onSecondary, + child: SizedBox( + height: 180.0, + child: InkWell( + onTap: onTap, + onLongPress: normalizeSecondary(onSecondary), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + CachedImage( + url: playlist!.image!.thumb, + width: 128.0, + height: 128.0, + rounded: true, + ), + Positioned( + bottom: 8.0, + left: 8.0, + child: PlayItemButton( + onTap: () async { + final Playlist fullPlaylist = + await deezerAPI.fullPlaylist(playlist!.id); + await playerHelper.playFromPlaylist(fullPlaylist); + }, + )) + ], + ), ), - ), - const SizedBox(height: 2.0), - SizedBox( - width: 144, - child: Text( - playlist!.title!, - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14.0), + const SizedBox(height: 2.0), + SizedBox( + width: 144, + child: Text( + playlist!.title!, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14.0), + ), ), - ), - const SizedBox( - height: 4.0, - ) - ], - ), - )); + const SizedBox( + height: 4.0, + ) + ], + ), + )), + ); } } @@ -794,18 +783,19 @@ class ShowTile extends StatelessWidget { class ShowEpisodeTile extends StatelessWidget { final ShowEpisode episode; - final Function? onTap; - final Function? onHold; + final VoidCallback? onTap; + final SecondaryTapCallback? onSecondary; final Widget? trailing; const ShowEpisodeTile(this.episode, - {super.key, this.onTap, this.onHold, this.trailing}); + {super.key, this.onTap, this.onSecondary, this.trailing}); @override Widget build(BuildContext context) { return InkWell( - onLongPress: onHold as void Function()?, - onTap: onTap as void Function()?, + onTap: onTap, + onLongPress: normalizeSecondary(onSecondary), + onSecondaryTapDown: onSecondary, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -817,7 +807,7 @@ class ShowEpisodeTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( episode.description!, - maxLines: 2, + maxLines: 10, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 897d1fc..5d470b8 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -123,6 +123,9 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# add app icon +install(FILES "app_icon.ico" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}") + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/linux/app_icon.ico b/linux/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8fbdc3da09c7400fb4edc02736c68ad23967a446 GIT binary patch literal 270622 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 E0A1;dart_entrypoint_arguments); - FlView* view = fl_view_new(project); + FlView *view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); @@ -63,16 +72,18 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); +static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status) +{ + MyApplication *self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) + { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); @@ -82,21 +93,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch } // Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); +static void my_application_dispose(GObject *object) +{ + MyApplication *self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init(MyApplicationClass* klass) { +static void my_application_class_init(MyApplicationClass *klass) +{ G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init(MyApplication* self) {} +static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, diff --git a/pubspec.lock b/pubspec.lock index b4422b9..f0f863b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -350,10 +350,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c" + sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" url: "https://pub.dev" source: hosted - version: "1.6.7" + version: "1.6.8" encrypt: dependency: "direct main" description: @@ -764,10 +764,10 @@ packages: description: path: "." ref: HEAD - resolved-ref: "8ccec63c67c0c206c6df3570e46f60b2a45dbb24" + resolved-ref: dcb7d1aa74a96b3dd892b3f65ec83f5d77352dfa url: "https://github.com/Pato05/just_audio_media_kit.git" source: git - version: "0.0.1" + version: "1.0.0" just_audio_platform_interface: dependency: transitive description: @@ -828,10 +828,10 @@ packages: dependency: transitive description: name: media_kit - sha256: "1283b500341d41f033478706204a2b4ae2612e9b331c934bc4fad8c4bb869f6d" + sha256: "3dffc6d0c19117d51fbc42a7f89612e0595665800a596289ab7a80bdd93e0ad1" url: "https://pub.dev" source: hosted - version: "1.1.8+2" + version: "1.1.9" media_kit_libs_linux: dependency: transitive description: @@ -1140,10 +1140,10 @@ packages: dependency: transitive description: name: quick_actions_android - sha256: f2ddc2c0cc5c001e87e62f6de06da18ebc75c6a06d26750f6f12276841c1585c + sha256: df67c20583e05f5038a24c47bfa1b7b2977703ec2d162663017c5f9ef8707699 url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" quick_actions_ios: dependency: transitive description: @@ -1197,18 +1197,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "2dafa6c1f8d8ee67b0e0587881947b78baab4671b7c08792cf91279e7ac14192" + sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "7.2.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" shelf: dependency: transitive description: @@ -1514,10 +1514,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "268e56b9c63f850406f54e9acb2a7d2ddf83c26c8ff9e7a125a96c3a513bf65f" + sha256: f45a6c03aa3f8322e0a9d7f4a0482721c8789cb41d555407367650b8f9c26018 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index b9ad76d..ecca0bb 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -6,10 +6,12 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"freezer", origin, size)) { + if (!window.Create(L"Freezer", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..8fbdc3da09c7400fb4edc02736c68ad23967a446 100644 GIT binary patch literal 270622 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 E0A1;|8^_Ub8Vp#`BLl3lbZ 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