diff --git a/lib/components/busy_wrapper.dart b/lib/components/busy_wrapper.dart new file mode 100644 index 0000000..c49906b --- /dev/null +++ b/lib/components/busy_wrapper.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:mopicon/utils/globals.dart'; + +/// Shows a modal busy indicator. +/// +/// If [busy] is `true`, interaction with [child] is disabled and +/// a progress indicator is displayed hovering on top of [child]. +/// The progress indicator is slowly fading in. +class BusyWrapper extends StatefulWidget { + final bool busy; + final Widget child; + + const BusyWrapper(this.child, this.busy, {super.key}); + + @override + State createState() => BusyWrapperState(); +} + +class BusyWrapperState extends State + with TickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + ); + + late final Animation _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ); + + bool _busy = false; + + bool get busy => _busy; + + set busy(bool b) { + setState(() { + b ? _controller.forward() : _controller.reset(); + _busy = b; + }); + } + + @override + initState() { + super.initState(); + _busy = widget.busy; + _controller.forward(); + } + + @override + dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant BusyWrapper oldWidget) { + _controller.reset(); + _controller.forward(); + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + if (!widget.busy) { + return widget.child; + } + + return Stack( + children: [ + widget.child, + Opacity( + opacity: 0.4, + child: ModalBarrier( + dismissible: false, + color: Globals.preferences.theme.data.dialogBackgroundColor), + ), + Center( + child: FadeTransition( + opacity: _animation, + child: const CircularProgressIndicator(), + ), + ), + ], + ); + } +} diff --git a/lib/pages/browse/library_browser_page.dart b/lib/pages/browse/library_browser_page.dart index 803835e..6202a2b 100644 --- a/lib/pages/browse/library_browser_page.dart +++ b/lib/pages/browse/library_browser_page.dart @@ -28,6 +28,7 @@ import 'package:mopicon/components/volume_control.dart'; import 'package:mopicon/services/mopidy_service.dart'; import 'package:mopicon/utils/parameters.dart'; import 'package:mopicon/components/action_buttons.dart'; +import 'package:mopicon/components/busy_wrapper.dart'; import 'package:mopicon/utils/globals.dart'; import 'package:mopicon/generated/l10n.dart'; import 'package:mopicon/extensions/mopidy_utils.dart'; @@ -52,6 +53,7 @@ class _LibraryBrowserPageState extends State { Ref? parent; List items = []; var images = {}; + bool showBusy = false; // selection mode (single/multiple) of track list view SelectionMode selectionMode = SelectionMode.off; @@ -88,6 +90,7 @@ class _LibraryBrowserPageState extends State { Future updateItems() async { try { + showBusy = true; if (widget.parent != null) { parent = Ref.fromMap(Parameter.fromBase64(widget.parent!)); } @@ -118,12 +121,13 @@ class _LibraryBrowserPageState extends State { var image = await item.getImage(); images.putIfAbsent(item.uri, () => image); } - + } catch (e, s) { + logger.e(e, stackTrace: s); + } finally { if (mounted) { + showBusy = false; setState(() {}); } - } catch (e, s) { - logger.e(e, stackTrace: s); } } @@ -180,40 +184,47 @@ class _LibraryBrowserPageState extends State { } }).build(); - return Scaffold( - appBar: AppBar( - title: Text(widget.title ?? S.of(context).libraryBrowserPageTitle), - centerTitle: true, - leading: widget.parent != null - ? ActionButton(Icons.arrow_back, () { - if (libraryController.selectionChanged.value.isEmpty) { - Navigator.of(context).pop(); - } else { - libraryController.unselect(); - } - }) - : null, - actions: [ - parent == null - ? ActionButton(Icons.delete, - () => libraryController.deleteSelectedPlaylists(), - valueListenable: libraryController.selectionChanged) - : const SizedBox(), - ActionButton(Icons.queue_music, () async { - var selectedItems = - await libraryController.getSelectedItems(parent); - await libraryController.addItemsToTracklist(selectedItems); - libraryController.unselect(); - }, valueListenable: libraryController.selectionChanged), - ActionButton(Icons.playlist_add, () async { - var selectedItems = - await libraryController.getSelectedItems(parent); - await libraryController.addItemsToPlaylist(selectedItems); - libraryController.unselect(); - }, valueListenable: libraryController.selectionChanged), - VolumeControl(), - LibraryBrowserAppBarMenu(items, libraryController) - ]), - body: MaterialPageFrame(child: listView)); + return BusyWrapper( + Scaffold( + appBar: AppBar( + title: + Text(widget.title ?? S.of(context).libraryBrowserPageTitle), + centerTitle: true, + leading: widget.parent != null + ? ActionButton(Icons.arrow_back, () { + if (libraryController.selectionChanged.value.isEmpty) { + Navigator.of(context).pop(); + } else { + libraryController.unselect(); + } + }) + : null, + actions: [ + parent == null + ? ActionButton(Icons.delete, + () => libraryController.deleteSelectedPlaylists(), + valueListenable: libraryController.selectionChanged) + : const SizedBox(), + ActionButton(Icons.queue_music, + () async { + var selectedItems = + await libraryController.getSelectedItems(parent); + await libraryController + .addItemsToTracklist(selectedItems); + libraryController.unselect(); + }, valueListenable: libraryController.selectionChanged), + ActionButton(Icons.playlist_add, + () async { + var selectedItems = + await libraryController.getSelectedItems(parent); + await libraryController + .addItemsToPlaylist(selectedItems); + libraryController.unselect(); + }, valueListenable: libraryController.selectionChanged), + VolumeControl(), + LibraryBrowserAppBarMenu(items, libraryController) + ]), + body: MaterialPageFrame(child: listView)), + showBusy); } } diff --git a/lib/pages/playlist/playlist_page.dart b/lib/pages/playlist/playlist_page.dart index 79243e6..0457549 100644 --- a/lib/pages/playlist/playlist_page.dart +++ b/lib/pages/playlist/playlist_page.dart @@ -25,6 +25,7 @@ import 'package:get_it/get_it.dart'; import 'package:flutter/material.dart'; import 'package:mopicon/components/material_page_frame.dart'; import 'package:mopicon/components/action_buttons.dart'; +import 'package:mopicon/components/busy_wrapper.dart'; import 'package:mopicon/components/volume_control.dart'; import 'package:mopicon/utils/globals.dart'; import 'package:mopicon/utils/parameters.dart'; @@ -54,6 +55,7 @@ class _PlaylistPageState extends State { late Ref playlist; List tracks = []; var images = {}; + bool showBusy = false; // selection mode (single/multiple) of track list view SelectionMode selectionMode = SelectionMode.off; @@ -61,24 +63,29 @@ class _PlaylistPageState extends State { final controller = GetIt.instance(); Future loadPlaylistItems() async { + List trx = []; try { + setState(() { + showBusy = true; + }); if (widget.parent != null) { playlist = Ref.fromMap(Parameter.fromBase64(widget.parent!)); controller.currentPlaylist = playlist; } - var trx = await controller.getPlaylistItems(playlist); + trx = await controller.getPlaylistItems(playlist); if (trx.isNotEmpty) { await loadImages(trx); - - if (mounted) { - setState(() { - tracks = trx; - }); - } } } catch (e, s) { logger.e(e, stackTrace: s); + } finally { + if (mounted) { + setState(() { + tracks = trx; + showBusy = false; + }); + } } } @@ -158,37 +165,45 @@ class _PlaylistPageState extends State { } }).buildListView(); - return Scaffold( - appBar: AppBar( - title: Text(widget.title ?? S.of(context).playlistPageTitle), - centerTitle: true, - //automaticallyImplyLeading: true, - leading: ActionButton(Icons.arrow_back, () { - if (controller.selectionChanged.value.isEmpty) { - Navigator.of(context).pop(); - } else { - controller.unselect(); - } - }), - actions: [ - ActionButton( - Icons.delete, - valueListenable: controller.selectionChanged, - () => controller.deleteSelectedPlaylistItems(playlist)), - ActionButton(Icons.queue_music, () async { - var selectedItems = await controller.getSelectedItems(playlist); - await controller.addItemsToTracklist(selectedItems.asRef); - controller.unselect(); - }, valueListenable: controller.selectionChanged), - ActionButton(Icons.playlist_add, () async { - var selectedItems = await controller.getSelectedItems(playlist); - await controller.addItemsToPlaylist(selectedItems.asRef); - controller.unselect(); - }, valueListenable: controller.selectionChanged), - VolumeControl(), - PlaylistAppBarMenu(controller, playlist) - ]), - body: MaterialPageFrame(child: listView), - ); + return BusyWrapper( + Scaffold( + appBar: AppBar( + title: Text(widget.title ?? S.of(context).playlistPageTitle), + centerTitle: true, + //automaticallyImplyLeading: true, + leading: + ActionButton(Icons.arrow_back, () { + if (controller.selectionChanged.value.isEmpty) { + Navigator.of(context).pop(); + } else { + controller.unselect(); + } + }), + actions: [ + ActionButton( + Icons.delete, + valueListenable: controller.selectionChanged, + () => controller.deleteSelectedPlaylistItems(playlist)), + ActionButton(Icons.queue_music, + () async { + var selectedItems = + await controller.getSelectedItems(playlist); + await controller + .addItemsToTracklist(selectedItems.asRef); + controller.unselect(); + }, valueListenable: controller.selectionChanged), + ActionButton(Icons.playlist_add, + () async { + var selectedItems = + await controller.getSelectedItems(playlist); + await controller.addItemsToPlaylist(selectedItems.asRef); + controller.unselect(); + }, valueListenable: controller.selectionChanged), + VolumeControl(), + PlaylistAppBarMenu(controller, playlist) + ]), + body: MaterialPageFrame(child: listView), + ), + showBusy); } } diff --git a/lib/pages/search/search_page.dart b/lib/pages/search/search_page.dart index 8ca4983..e61ef2e 100644 --- a/lib/pages/search/search_page.dart +++ b/lib/pages/search/search_page.dart @@ -32,6 +32,7 @@ import 'package:mopicon/services/mopidy_service.dart'; import 'package:mopicon/components/reorderable_list_view.dart'; import 'package:mopicon/components/selected_item_positions.dart'; import 'package:mopicon/components/action_buttons.dart'; +import 'package:mopicon/components/busy_wrapper.dart'; import 'package:mopicon/components/item_action_dialog.dart'; import 'search_view_controller.dart'; @@ -51,6 +52,7 @@ class _SearchPageState extends State { List tracks = []; var images = {}; + bool showBusy = false; // selection mode (single/multiple) of track list view SelectionMode selectionMode = SelectionMode.off; @@ -133,17 +135,26 @@ class _SearchPageState extends State { padding: const MaterialStatePropertyAll( EdgeInsets.symmetric(horizontal: 16.0)), onSubmitted: (String value) async { - List searchResult = - await _mopidyService.search(SearchCriteria().any([value])); - var trx = searchResult.first.tracks; - if (trx.isNotEmpty) { - loadImages(trx); - } else { - trx = []; + List trx = []; + try { + setState(() { + showBusy = true; + }); + + List searchResult = + await _mopidyService.search(SearchCriteria().any([value])); + trx = searchResult.first.tracks; + if (trx.isNotEmpty) { + await loadImages(trx); + } + } catch (e, s) { + logger.e(e, stackTrace: s); + } finally { + setState(() { + showBusy = false; + tracks = trx; + }); } - setState(() { - tracks = trx; - }); }, leading: const Icon(Icons.search), trailing: [ @@ -151,6 +162,7 @@ class _SearchPageState extends State { onPressed: () { textEditingController.clear(); setState(() { + showBusy = false; tracks = []; }); }, @@ -167,34 +179,40 @@ class _SearchPageState extends State { child: Text(S.of(context).searchPageNotSupportedMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16))); - return Scaffold( - appBar: AppBar( - title: Text(S.of(context).searchPageTitle), - centerTitle: true, - leading: controller.selectionChanged.value.isNotEmpty - ? ActionButton(Icons.arrow_back, () { + return BusyWrapper( + Scaffold( + appBar: AppBar( + title: Text(S.of(context).searchPageTitle), + centerTitle: true, + leading: controller.selectionChanged.value.isNotEmpty + ? ActionButton(Icons.arrow_back, () { + controller.unselect(); + }) + : null, + actions: [ + ActionButton(Icons.queue_music, + () async { + var selectedItems = + controller.selectionChanged.value.filterSelected(tracks); + await controller + .addItemsToTracklist(selectedItems.asRef); controller.unselect(); - }) - : null, - actions: [ - ActionButton(Icons.queue_music, () async { - var selectedItems = - controller.selectionChanged.value.filterSelected(tracks); - await controller.addItemsToTracklist(selectedItems.asRef); - controller.unselect(); - }, valueListenable: controller.selectionChanged), - ActionButton(Icons.playlist_add, () async { - var selectedItems = - controller.selectionChanged.value.filterSelected(tracks); - await controller.addItemsToPlaylist(selectedItems.asRef); - controller.unselect(); - }, valueListenable: controller.selectionChanged), - VolumeControl(), - SearchAppBarMenu(tracks.length, controller) - ]), - body: MaterialPageFrame( - child: - Globals.preferences.searchSupported ? pageContent : notSupported), - ); + }, valueListenable: controller.selectionChanged), + ActionButton(Icons.playlist_add, + () async { + var selectedItems = + controller.selectionChanged.value.filterSelected(tracks); + await controller.addItemsToPlaylist(selectedItems.asRef); + controller.unselect(); + }, valueListenable: controller.selectionChanged), + VolumeControl(), + SearchAppBarMenu(tracks.length, controller) + ]), + body: MaterialPageFrame( + child: Globals.preferences.searchSupported + ? pageContent + : notSupported), + ), + showBusy); } } diff --git a/lib/pages/tracklist/tracklist_page.dart b/lib/pages/tracklist/tracklist_page.dart index d56c9f1..5ff6ac7 100644 --- a/lib/pages/tracklist/tracklist_page.dart +++ b/lib/pages/tracklist/tracklist_page.dart @@ -35,6 +35,7 @@ import 'package:mopicon/extensions/mopidy_utils.dart'; import 'package:mopicon/components/reorderable_list_view.dart'; import 'package:mopicon/components/selected_item_positions.dart'; import 'package:mopicon/components/item_action_dialog.dart'; +import 'package:mopicon/components/busy_wrapper.dart'; import 'tracklist_view_controller.dart'; import 'tracklist_appbar_menu.dart'; @@ -52,6 +53,7 @@ class _TrackListState extends State { // all tracks on the tracklist List tracks = []; var images = {}; + bool showBusy = false; // currently active track int? playingTlId; @@ -93,8 +95,12 @@ class _TrackListState extends State { // updates track list view from current track list and // updates cover thumbnails void updateTracks() async { + List trks = []; try { - var trks = await controller.loadTrackList(); + setState(() { + showBusy = true; + }); + trks = await controller.loadTrackList(); // load images into local map for (TlTrack tlt in trks) { @@ -103,12 +109,13 @@ class _TrackListState extends State { images.putIfAbsent(tlt.track.uri, () => image); } } - + } catch (e, s) { + logger.e(e, stackTrace: s); + } finally { setState(() { + showBusy = false; tracks = trks; }); - } catch (e, s) { - logger.e(e, stackTrace: s); } } @@ -340,28 +347,32 @@ class _TrackListState extends State { ? [Expanded(child: listView), currentlyPlayingPanel] : [currentlyPlayingPanel]; - return Scaffold( - appBar: AppBar( - title: Text(S.of(context).trackListPageTitle), - centerTitle: true, - leading: ActionButton(Icons.arrow_back, - valueListenable: controller.selectionChanged, () { - controller.unselect(); - }), - actions: [ - ActionButton( - Icons.delete, - valueListenable: controller.selectionChanged, - controller.deleteSelectedTracks), - ActionButton(Icons.playlist_add, () async { - var selectedItems = await controller.getSelectedItems(); - await controller.addItemsToPlaylist(selectedItems); - controller.unselect(); - }, valueListenable: controller.selectionChanged), - VolumeControl(), - TracklistAppBarMenu(controller) - ]), - body: MaterialPageFrame( - child: Column(mainAxisSize: MainAxisSize.max, children: children))); + return BusyWrapper( + Scaffold( + appBar: AppBar( + title: Text(S.of(context).trackListPageTitle), + centerTitle: true, + leading: ActionButton(Icons.arrow_back, + valueListenable: controller.selectionChanged, () { + controller.unselect(); + }), + actions: [ + ActionButton( + Icons.delete, + valueListenable: controller.selectionChanged, + controller.deleteSelectedTracks), + ActionButton(Icons.playlist_add, + () async { + var selectedItems = await controller.getSelectedItems(); + await controller.addItemsToPlaylist(selectedItems); + controller.unselect(); + }, valueListenable: controller.selectionChanged), + VolumeControl(), + TracklistAppBarMenu(controller) + ]), + body: MaterialPageFrame( + child: Column( + mainAxisSize: MainAxisSize.max, children: children))), + showBusy); } }