From 322fadd2d403d7be5a42d0593d9ed8a8de52b1fa Mon Sep 17 00:00:00 2001 From: "Kern, Thomas" Date: Sat, 13 Jan 2024 18:17:34 +0100 Subject: [PATCH] Improve handling of reconnects with modal barrier overlay. Special routes and screen for connecting removed. --- lib/components/busy_wrapper.dart | 88 +++- lib/components/titled_divider.dart | 16 +- lib/initializer.dart | 2 - lib/pages/browse/library_browser_page.dart | 87 ++-- lib/pages/browse/library_list_view.dart | 3 +- .../connecting_screen/connecting_screen.dart | 66 --- .../connecting_screen_controller.dart | 79 --- lib/pages/home/home_page.dart | 98 ++-- lib/pages/playlist/playlist_page.dart | 8 +- lib/pages/search/search_page.dart | 78 +-- .../settings/preferences_controller.dart | 13 - lib/pages/settings/preferences_page.dart | 14 +- lib/pages/tracklist/tracklist_page.dart | 128 ++--- lib/routes/application_routes.dart | 44 +- lib/services/cover_service.dart | 23 +- lib/services/mopidy_service.dart | 449 +++++++++++------- lib/utils/open_value_notifier.dart | 2 +- 17 files changed, 594 insertions(+), 604 deletions(-) delete mode 100644 lib/pages/connecting_screen/connecting_screen.dart delete mode 100644 lib/pages/connecting_screen/connecting_screen_controller.dart diff --git a/lib/components/busy_wrapper.dart b/lib/components/busy_wrapper.dart index c8d920a..de821ab 100644 --- a/lib/components/busy_wrapper.dart +++ b/lib/components/busy_wrapper.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:mopicon/pages/settings/preferences_controller.dart'; +import 'package:mopicon/services/mopidy_service.dart'; +import 'package:mopicon/common/globals.dart'; +import 'package:mopicon/generated/l10n.dart'; /// Shows a modal busy indicator. /// @@ -19,24 +22,45 @@ class BusyWrapper extends StatefulWidget { class _BusyWrapperState extends State with TickerProviderStateMixin { final preferences = GetIt.instance(); + final mopidyService = GetIt.instance(); - late final AnimationController _controller = AnimationController( + late final AnimationController _primaryController = AnimationController( duration: const Duration(seconds: 4), vsync: this, ); - late final Animation _animation = CurvedAnimation( - parent: _controller, + late final AnimationController _secondaryController = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + ); + + late final Animation _connectionAnimation = CurvedAnimation( + parent: _secondaryController, + curve: Curves.easeIn, + ); + + late final Animation _busyAnimation = CurvedAnimation( + parent: _primaryController, curve: Curves.easeIn, ); + void _startAnimation() { + _primaryController.forward(from: 0); + _secondaryController.forward(from: 0); + } + + void _stopAnimation() { + _primaryController.reset(); + _secondaryController.reset(); + } + bool _busy = false; bool get busy => _busy; set busy(bool b) { setState(() { - b ? _controller.forward(from: 0) : _controller.reset(); + b ? _startAnimation() : _stopAnimation(); _busy = b; }); } @@ -45,42 +69,72 @@ class _BusyWrapperState extends State with TickerProviderStateMixin initState() { super.initState(); _busy = widget.busy; - _controller.forward(from: 0); + _startAnimation(); } @override dispose() { - _controller.dispose(); + _primaryController.dispose(); + _secondaryController.dispose(); super.dispose(); } @override void didUpdateWidget(covariant BusyWrapper oldWidget) { - _controller.reset(); - _controller.forward(from: 0); + _startAnimation(); super.didUpdateWidget(oldWidget); } + void stop() async { + _stopAnimation(); + mopidyService.stop(); + Globals.applicationRoutes.gotoSettings(); + } + @override Widget build(BuildContext context) { - if (!widget.busy) { + if (!widget.busy && mopidyService.connected) { return widget.child; } - return Stack( + return Material( + child: Stack( children: [ widget.child, Opacity( - opacity: 0.4, + opacity: 0.5, child: ModalBarrier(dismissible: false, color: preferences.theme.data.dialogBackgroundColor), ), - Center( - child: FadeTransition( - opacity: _animation, - child: const CircularProgressIndicator(), - ), + FadeTransition( + opacity: _busyAnimation, + child: Center(child: Container(padding: const EdgeInsets.all(40), child: const CircularProgressIndicator())), ), + mopidyService.connected == false + ? Center( + child: FadeTransition( + opacity: _connectionAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).connectingPageConnecting, + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 100), + ElevatedButton( + onPressed: stop, + child: Text(S.of(context).connectingPageStopBtn, + style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold))) + ], + ), + ), + ) + : const SizedBox(), ], - ); + )); } } diff --git a/lib/components/titled_divider.dart b/lib/components/titled_divider.dart index 771a1dd..f91599b 100644 --- a/lib/components/titled_divider.dart +++ b/lib/components/titled_divider.dart @@ -39,9 +39,7 @@ class TitledDivider extends StatelessWidget { child: Row(children: [ Text(title), Expanded( - child: Container( - margin: const EdgeInsets.only(left: 10.0), - child: const Divider()), + child: Container(margin: const EdgeInsets.only(left: 10.0), child: const Divider()), ), ])); } @@ -54,24 +52,22 @@ class SpacedDivider extends StatelessWidget { @override build(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: _defaultTop, bottom: _defaultBottom), - child: const Divider()); + return Container(margin: EdgeInsets.only(top: _defaultTop, bottom: _defaultBottom), child: const Divider()); } } /// A vertical spaced. class VerticalSpacer extends StatelessWidget { - double? space; + final double? space; /// Creates a [Divider] with additional vertical spacing. - VerticalSpacer({this.space, super.key}); + const VerticalSpacer({this.space, super.key}); @override build(BuildContext context) { - space = space ?? _defaultTop + _defaultBottom; + var vsp = space ?? _defaultTop + _defaultBottom; return Container( - margin: EdgeInsets.only(top: space! / 2.0, bottom: space! / 2), + margin: EdgeInsets.only(top: vsp / 2.0, bottom: vsp / 2), ); } } diff --git a/lib/initializer.dart b/lib/initializer.dart index b085f49..bc4dee5 100644 --- a/lib/initializer.dart +++ b/lib/initializer.dart @@ -25,7 +25,6 @@ import 'package:get_it/get_it.dart'; import 'package:mopicon/pages/settings/preferences_controller.dart'; import 'package:mopicon/services/mopidy_service.dart'; import 'package:mopicon/services/cover_service.dart'; -import 'package:mopicon/pages/connecting_screen/connecting_screen_controller.dart'; import 'package:mopicon/pages/browse/library_browser_controller.dart'; import 'package:mopicon/pages/playlist/playlist_view_controller.dart'; import 'package:mopicon/pages/tracklist/tracklist_view_controller.dart'; @@ -48,7 +47,6 @@ class Initializer { Globals.logger.i("starting registering services"); getIt.registerLazySingleton(() => PreferencesControllerImpl()); getIt.registerLazySingleton(() => MopidyServiceImpl()); - getIt.registerLazySingleton(() => ConnectingScreenControllerImpl()); getIt.registerLazySingleton(() => CoverServiceImpl()); getIt.registerLazySingleton(() => LibraryBrowserControllerImpl()); getIt.registerLazySingleton(() => TracklistViewControllerImpl()); diff --git a/lib/pages/browse/library_browser_page.dart b/lib/pages/browse/library_browser_page.dart index 7678d59..20037f7 100644 --- a/lib/pages/browse/library_browser_page.dart +++ b/lib/pages/browse/library_browser_page.dart @@ -29,7 +29,6 @@ import 'package:mopicon/services/mopidy_service.dart'; import 'package:mopicon/pages/settings/preferences_controller.dart'; import 'package:mopicon/utils/parameters.dart'; import 'package:mopicon/components/action_buttons.dart'; -import 'package:mopicon/components/busy_wrapper.dart'; import 'package:mopicon/common/globals.dart'; import 'package:mopicon/generated/l10n.dart'; import 'package:mopicon/extensions/mopidy_utils.dart'; @@ -54,7 +53,6 @@ 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; @@ -69,9 +67,7 @@ class _LibraryBrowserPageState extends State { Future updateItems() async { try { - setState(() { - showBusy = true; - }); + mopidyService.setBusy(true); if (widget.parent != null) { parent = Ref.fromMap(Parameter.fromBase64(widget.parent!)); } @@ -93,11 +89,8 @@ class _LibraryBrowserPageState extends State { } catch (e, s) { Globals.logger.e(e, stackTrace: s); } finally { - if (mounted) { - setState(() { - showBusy = false; - }); - } + mopidyService.setBusy(false); + setState(() {}); } } @@ -158,45 +151,43 @@ class _LibraryBrowserPageState extends State { } }).build(); - 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.isSelectionEmpty) { - Navigator.of(context).pop(); - } else { - libraryController.notifyUnselect(); - } - }) - : null, - actions: [ - parent == null - ? ActionButton( - Icons.delete, () => libraryController.deleteSelectedPlaylists(context), - valueListenable: libraryController.selectionChanged) - : const SizedBox(), - ActionButton(Icons.queue_music, () async { - var selectedItems = await libraryController.getSelectedItems(parent); - if (context.mounted) { - await libraryController.addItemsToTracklist(context, selectedItems); - } - libraryController.notifyUnselect(); - }, valueListenable: libraryController.selectionChanged), - ActionButton(Icons.playlist_add, () async { - var selectedItems = await libraryController.getSelectedItems(parent); - if (context.mounted) { - await libraryController.addItemsToPlaylist(context, selectedItems); + return Scaffold( + appBar: AppBar( + title: Text(widget.title ?? S.of(context).libraryBrowserPageTitle), + centerTitle: true, + leading: widget.parent != null + ? ActionButton(Icons.arrow_back, () { + if (libraryController.isSelectionEmpty) { + Navigator.of(context).pop(); + } else { + libraryController.notifyUnselect(); } - libraryController.notifyUnselect(); - }, valueListenable: libraryController.selectionChanged), - VolumeControl(), - LibraryBrowserAppBarMenu(items, libraryController) - ]), - body: MaterialPageFrame(child: listView)), - showBusy); + }) + : null, + actions: [ + parent == null + ? ActionButton( + Icons.delete, () => libraryController.deleteSelectedPlaylists(context), + valueListenable: libraryController.selectionChanged) + : const SizedBox(), + ActionButton(Icons.queue_music, () async { + var selectedItems = await libraryController.getSelectedItems(parent); + if (context.mounted) { + await libraryController.addItemsToTracklist(context, selectedItems); + } + libraryController.notifyUnselect(); + }, valueListenable: libraryController.selectionChanged), + ActionButton(Icons.playlist_add, () async { + var selectedItems = await libraryController.getSelectedItems(parent); + if (context.mounted) { + await libraryController.addItemsToPlaylist(context, selectedItems); + } + libraryController.notifyUnselect(); + }, valueListenable: libraryController.selectionChanged), + VolumeControl(), + LibraryBrowserAppBarMenu(items, libraryController) + ]), + body: MaterialPageFrame(child: listView)); } void translateNames(BuildContext context) { diff --git a/lib/pages/browse/library_list_view.dart b/lib/pages/browse/library_list_view.dart index 7a918dd..534c42d 100644 --- a/lib/pages/browse/library_list_view.dart +++ b/lib/pages/browse/library_list_view.dart @@ -62,7 +62,8 @@ class LibraryListView { ), ); } else if (item.type == Ref.typeTrack) { - return images[getUri(item)]!; + Widget? w = images[getUri(item)]; + return w ?? ImageUtils.getIconForType(item.uri, item.type); } else { return ImageUtils.getIconForType(item.uri, item.type); } diff --git a/lib/pages/connecting_screen/connecting_screen.dart b/lib/pages/connecting_screen/connecting_screen.dart deleted file mode 100644 index dc94e0e..0000000 --- a/lib/pages/connecting_screen/connecting_screen.dart +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2023 Thomas Kern - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'connecting_screen_controller.dart'; -import 'package:mopicon/generated/l10n.dart'; - -class ConnectingScreen extends StatefulWidget { - const ConnectingScreen({super.key}); - - @override - State createState() => _ConnectingScreenState(); -} - -class _ConnectingScreenState extends State { - final controller = GetIt.instance(); - - @override - void initState() { - super.initState(); - controller.connect(maxRetries: 6); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context).connectingPageConnecting, - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - ), - ), - //const SizedBox(height: 20), - Container(padding: const EdgeInsets.all(40), child: const CircularProgressIndicator()), - ElevatedButton( - onPressed: controller.stop, - child: Text(S.of(context).connectingPageStopBtn, - style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold))) - ], - ), - ); - } -} diff --git a/lib/pages/connecting_screen/connecting_screen_controller.dart b/lib/pages/connecting_screen/connecting_screen_controller.dart deleted file mode 100644 index ec1c6df..0000000 --- a/lib/pages/connecting_screen/connecting_screen_controller.dart +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2023 Thomas Kern - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ -import 'package:mopicon/common/base_controller.dart'; -import 'package:mopicon/services/mopidy_service.dart'; -import 'package:get_it/get_it.dart'; -import 'package:mopicon/common/globals.dart'; -import 'package:mopicon/pages/settings/preferences_controller.dart'; - -abstract class ConnectingScreenController extends BaseController { - void connect({int maxRetries}); - - void stop(); - - bool get retriesExceeded; -} - -class ConnectingScreenControllerImpl extends ConnectingScreenController { - int? _maxRetries; - int _retries = 0; - - final _preferences = GetIt.instance(); - - ConnectingScreenControllerImpl() { - void connectionListener(MopidyConnectionState state) async { - if (state == MopidyConnectionState.reconnecting) { - _retries++; - } else if (state == MopidyConnectionState.online) { - // Search is only supported if the Mopidy-Local extension is - // enabled for the server. Just set a flag we can - // check later. - _preferences.searchSupported = (await mopidyService.getUriSchemes()).contains('local'); - Globals.applicationRoutes.gotoHome(); - } else { - Globals.applicationRoutes.gotoConnecting(); - } - } - - mopidyService.connectionState$.listen(connectionListener); - } - - @override - bool get retriesExceeded => _maxRetries != null && _retries >= _maxRetries!; - - @override - void connect({int? maxRetries}) async { - _maxRetries = maxRetries; - _retries = 0; - mopidyService.stop(); - bool success = await mopidyService.connect(_preferences.url, maxRetries: maxRetries); - if (!success) { - Globals.applicationRoutes.gotoSettings(); - } - } - - @override - void stop() async { - mopidyService.stop(); - Globals.applicationRoutes.gotoSettings(); - } -} diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 8f3de78..12ca304 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -19,14 +19,20 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mopicon/generated/l10n.dart'; import 'package:mopicon/services/mopidy_service.dart'; +import 'package:mopicon/components/busy_wrapper.dart'; import 'package:get_it/get_it.dart'; +import '../settings/preferences_controller.dart'; + class HomeView extends StatefulWidget { final StatefulNavigationShell navigationShell; + const HomeView(this.navigationShell, {super.key}); @override @@ -37,12 +43,18 @@ class _HomeViewState extends State { final _mopidyService = GetIt.instance(); int trackListCount = 0; + bool _showBusy = true; + + StreamSubscription? connectionSubscription; + StreamSubscription? busySubscription; void initTrackListCount() async { int count = await _mopidyService.getTracklistLength(); - setState(() { - trackListCount = count; - }); + if (mounted) { + setState(() { + trackListCount = count; + }); + } } void updateTrackListCount() { @@ -56,49 +68,65 @@ class _HomeViewState extends State { @override void initState() { super.initState(); + GetIt.instance().connect(GetIt.instance().url); + _showBusy = true; + connectionSubscription = _mopidyService.connectionState$.listen((MopidyConnectionState state) { + setState(() { + _showBusy = state != MopidyConnectionState.online; + }); + }); + busySubscription = _mopidyService.busyState$.listen((bool busy) { + setState(() { + _showBusy = busy ? true : !(!busy && _mopidyService.connected); + }); + }); _mopidyService.tracklistChangedNotifier.addListener(updateTrackListCount); initTrackListCount(); } @override void dispose() { + connectionSubscription?.cancel(); + busySubscription?.cancel(); _mopidyService.tracklistChangedNotifier.removeListener(updateTrackListCount); super.dispose(); } @override Widget build(BuildContext context) { - return Scaffold( - body: widget.navigationShell, - bottomNavigationBar: BottomNavigationBar( - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.search), - label: S.of(context).homePageSearchLbl, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.library_music), - label: S.of(context).homePageBrowseLbl, - ), - BottomNavigationBarItem( - icon: Badge( - isLabelVisible: trackListCount > 0, - label: Text('$trackListCount'), - child: const Icon(Icons.queue_music), - ), - label: S.of(context).homePageTracksLbl, - ), - ], - currentIndex: widget.navigationShell.currentIndex, - onTap: (int index) { - widget.navigationShell.goBranch( - index, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: index == widget.navigationShell.currentIndex, - ); - })); + return BusyWrapper( + Scaffold( + body: widget.navigationShell, + bottomNavigationBar: BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.search), + label: S.of(context).homePageSearchLbl, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.library_music), + label: S.of(context).homePageBrowseLbl, + ), + BottomNavigationBarItem( + icon: Badge( + isLabelVisible: trackListCount > 0, + label: Text('$trackListCount'), + child: const Icon(Icons.queue_music), + ), + label: S.of(context).homePageTracksLbl, + ), + ], + currentIndex: widget.navigationShell.currentIndex, + onTap: (int index) { + widget.navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == widget.navigationShell.currentIndex, + ); + })), + _showBusy); } } diff --git a/lib/pages/playlist/playlist_page.dart b/lib/pages/playlist/playlist_page.dart index 9f4c750..33ae0d8 100644 --- a/lib/pages/playlist/playlist_page.dart +++ b/lib/pages/playlist/playlist_page.dart @@ -50,8 +50,6 @@ class PlaylistPage extends StatefulWidget { } class _PlaylistPageState extends State { - final mopidyService = GetIt.instance(); - late Ref playlist; List tracks = []; var images = {}; @@ -140,9 +138,9 @@ class _PlaylistPageState extends State { (int start, int current) async { try { if (start < current) { - await mopidyService.movePlaylistItem(playlist, start, current - 1); + await controller.mopidyService.movePlaylistItem(playlist, start, current - 1); } else { - await mopidyService.movePlaylistItem(playlist, start, current); + await controller.mopidyService.movePlaylistItem(playlist, start, current); } } catch (e) { Globals.logger.e(e); @@ -154,7 +152,7 @@ class _PlaylistPageState extends State { switch (r) { case ItemActionOption.play: await controller.addItemsToTracklist(context, [track.asRef]); - mopidyService.play(track.asRef); + controller.mopidyService.play(track.asRef); break; case ItemActionOption.addToTracklist: await controller.addItemsToTracklist(context, [track.asRef]); diff --git a/lib/pages/search/search_page.dart b/lib/pages/search/search_page.dart index b8b7484..13e7535 100644 --- a/lib/pages/search/search_page.dart +++ b/lib/pages/search/search_page.dart @@ -29,11 +29,9 @@ import 'package:mopicon/components/volume_control.dart'; import 'package:mopicon/common/globals.dart'; import 'package:mopicon/generated/l10n.dart'; import 'package:mopicon/services/mopidy_service.dart'; -import 'package:mopicon/pages/settings/preferences_controller.dart'; import 'package:mopicon/components/reorderable_list_view.dart'; import 'package:mopicon/common/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'; @@ -49,12 +47,12 @@ class SearchPage extends StatefulWidget { class _SearchPageState extends State { final mopidyService = GetIt.instance(); - final preferences = GetIt.instance(); final TextEditingController textEditingController = TextEditingController(); List tracks = []; var images = {}; - bool showBusy = false; + + bool searchSupported = false; // selection mode (single/multiple) of track list view SelectionMode selectionMode = SelectionMode.off; @@ -86,9 +84,22 @@ class _SearchPageState extends State { //WidgetsBinding.instance.addPostFrameCallback((_) => setState(() { })); } + void checkSearchSupported() async { + if (mounted) { + // Search is only supported if the Mopidy-Local extension is + // enabled for the server. Just set a flag we can + // check later. + var canSearch = (await mopidyService.getUriSchemes()).contains('local'); + setState(() { + searchSupported = canSearch; + }); + } + } + @override void initState() { super.initState(); + checkSearchSupported(); controller.selectionModeChanged.addListener(updateSelection); controller.selectionChanged.addListener(updateSelection); updateSelection(); @@ -132,9 +143,7 @@ class _SearchPageState extends State { onSubmitted: (String value) async { List trx = []; try { - setState(() { - showBusy = true; - }); + controller.mopidyService.setBusy(true); List searchResult = await mopidyService.search(SearchCriteria().any([value])); trx = searchResult.first.tracks; @@ -144,8 +153,8 @@ class _SearchPageState extends State { } catch (e, s) { Globals.logger.e(e, stackTrace: s); } finally { + controller.mopidyService.setBusy(false); setState(() { - showBusy = false; tracks = trx; }); } @@ -156,7 +165,6 @@ class _SearchPageState extends State { onPressed: () { textEditingController.clear(); setState(() { - showBusy = false; tracks = []; }); }, @@ -167,36 +175,34 @@ class _SearchPageState extends State { Expanded(child: Padding(padding: const EdgeInsets.only(top: 12), child: listView)) ]); - var notSupported = Expanded( + var notSupported = Center( child: Text(S.of(context).searchPageNotSupportedMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16))); - return BusyWrapper( - Scaffold( - appBar: AppBar( - title: Text(S.of(context).searchPageTitle), - centerTitle: true, - leading: controller.selectionChanged.value.isNotEmpty - ? ActionButton(Icons.arrow_back, () { - controller.notifyUnselect(); - }) - : null, - actions: [ - ActionButton(Icons.queue_music, () async { - var selectedItems = controller.selectionChanged.value.filterSelected(tracks); - await controller.addItemsToTracklist(context, selectedItems.asRef); - controller.notifyUnselect(); - }, valueListenable: controller.selectionChanged), - ActionButton(Icons.playlist_add, () async { - var selectedItems = controller.selectionChanged.value.filterSelected(tracks); - await controller.addItemsToPlaylist(context, selectedItems.asRef); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).searchPageTitle), + centerTitle: true, + leading: controller.selectionChanged.value.isNotEmpty + ? ActionButton(Icons.arrow_back, () { controller.notifyUnselect(); - }, valueListenable: controller.selectionChanged), - VolumeControl(), - SearchAppBarMenu(tracks.length, controller) - ]), - body: MaterialPageFrame(child: preferences.searchSupported ? pageContent : notSupported), - ), - showBusy); + }) + : null, + actions: [ + ActionButton(Icons.queue_music, () async { + var selectedItems = controller.selectionChanged.value.filterSelected(tracks); + await controller.addItemsToTracklist(context, selectedItems.asRef); + controller.notifyUnselect(); + }, valueListenable: controller.selectionChanged), + ActionButton(Icons.playlist_add, () async { + var selectedItems = controller.selectionChanged.value.filterSelected(tracks); + await controller.addItemsToPlaylist(context, selectedItems.asRef); + controller.notifyUnselect(); + }, valueListenable: controller.selectionChanged), + VolumeControl(), + SearchAppBarMenu(tracks.length, controller) + ]), + body: MaterialPageFrame(child: searchSupported ? pageContent : notSupported), + ); } } diff --git a/lib/pages/settings/preferences_controller.dart b/lib/pages/settings/preferences_controller.dart index 12b46db..3ba8420 100644 --- a/lib/pages/settings/preferences_controller.dart +++ b/lib/pages/settings/preferences_controller.dart @@ -71,10 +71,6 @@ abstract class PreferencesController { bool get showAllMediaCategories; set showAllMediaCategories(bool f); - - bool get searchSupported; - - set searchSupported(bool v); } class PreferencesControllerImpl extends PreferencesController { @@ -107,15 +103,6 @@ class PreferencesControllerImpl extends PreferencesController { @override String get version => _version; - /// Flag to indicate that search is supported. This is set to true - /// after a connection has been established and the Mopidy server - /// has the Mopidy-Local extension enabled. - bool _searchSupported = false; - - bool get searchSupported => _searchSupported; - - set searchSupported(bool v) => _searchSupported = v; - @override Future load() async { var prefs = await SharedPreferences.getInstance(); diff --git a/lib/pages/settings/preferences_page.dart b/lib/pages/settings/preferences_page.dart index 5ba87bd..d50a379 100644 --- a/lib/pages/settings/preferences_page.dart +++ b/lib/pages/settings/preferences_page.dart @@ -21,7 +21,6 @@ */ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; -import 'package:mopicon/pages/connecting_screen/connecting_screen_controller.dart'; import 'package:mopicon/components/material_page_frame.dart'; import 'package:mopicon/components/titled_divider.dart'; import 'package:mopicon/components/error_snackbar.dart'; @@ -43,7 +42,6 @@ class _PreferencesState extends State { final preferences = GetIt.instance(); final mopidyService = GetIt.instance(); final preferencesFormKey = GlobalKey(debugLabel: "preferencesPage"); - final _connectingController = GetIt.instance(); AppLocale? newLocale; String? originalUri; @@ -69,7 +67,6 @@ class _PreferencesState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback(postRetryError); load(); } @@ -79,12 +76,6 @@ class _PreferencesState extends State { super.dispose(); } - void postRetryError(_) { - if (_connectingController.retriesExceeded) { - showError(S.of(context).preferencesPageConnectErrorTitle, S.of(context).preferencesPageConnectErrorDetails); - } - } - @override Widget build(BuildContext context) { final List> themes = >[]; @@ -128,13 +119,14 @@ class _PreferencesState extends State { try { preferences.appLocale = newLocale ?? preferences.appLocale; await save(); - S.load(preferences.appLocale.locale); // disconnect if connection changed if (originalUri != preferences.url) { if (mounted) { mopidyService.stop(); + GetIt.instance().connect(preferences.url); } } + S.load(preferences.appLocale.locale); Globals.applicationRoutes.gotoHome(); } catch (e) { showError(preferencesPageSaveError, null); @@ -177,7 +169,7 @@ class _PreferencesState extends State { : S.of(context).preferencesPageMopidyServerInvalid; }, ), - VerticalSpacer(), + const VerticalSpacer(), TextFormField( keyboardType: TextInputType.number, initialValue: preferences.port != null ? preferences.port.toString() : '', diff --git a/lib/pages/tracklist/tracklist_page.dart b/lib/pages/tracklist/tracklist_page.dart index 5c7ebdc..08d10f2 100644 --- a/lib/pages/tracklist/tracklist_page.dart +++ b/lib/pages/tracklist/tracklist_page.dart @@ -41,7 +41,6 @@ import 'package:mopicon/common/selected_item_positions.dart'; import 'package:mopicon/components/item_action_dialog.dart'; import 'tracklist_view_controller.dart'; import 'tracklist_appbar_menu.dart'; -import 'package:mopicon/components/busy_wrapper.dart'; class TrackListPage extends StatefulWidget { const TrackListPage({super.key}); @@ -52,7 +51,6 @@ class TrackListPage extends StatefulWidget { class _TrackListState extends State { final controller = GetIt.instance(); - final mopidyService = GetIt.instance(); // all tracks on the tracklist List tracks = []; @@ -80,8 +78,6 @@ class _TrackListState extends State { // If false, NowPlaying covers whole window and is showing more details. bool splitEnabled = true; - bool showBusy = false; - StreamSubscription? refreshSubscription; TlTrack? getTrackByTlid(int? tlid) { @@ -103,9 +99,8 @@ class _TrackListState extends State { void updateTracks() async { List trks = []; try { - setState(() { - showBusy = true; - }); + controller.mopidyService.setBusy(true); + setState(() {}); trks = await controller.loadTrackList(); // load images into local map @@ -118,41 +113,49 @@ class _TrackListState extends State { } catch (e, s) { Globals.logger.e(e, stackTrace: s); } finally { - setState(() { - tracks = trks; - showBusy = false; - }); + controller.mopidyService.setBusy(false); + if (mounted) { + setState(() { + tracks = trks; + //showBusy = false; + }); + } } } // updates current track view, playback state und position within track. void updatePlayback() async { try { - final tlTrack = await mopidyService.getCurrentTlTrack(); + controller.mopidyService.setBusy(true); + final tlTrack = await controller.mopidyService.getCurrentTlTrack(); String? strTitle; if (tlTrack != null && tlTrack.track.uri.isStreamUri()) { - strTitle = await mopidyService.getStreamTitle(); + strTitle = await controller.mopidyService.getStreamTitle(); } - final state = await mopidyService.getPlaybackState(); - final position = await mopidyService.getTimePosition(); - setState(() { - playingTlId = - state != null && (state == PlaybackState.playing || state == PlaybackState.paused) ? tlTrack?.tlid : null; - playbackState = state ?? playbackState; - timePosition = position ?? 0; - streamTitle = strTitle; - isStream = tlTrack != null ? tlTrack.track.uri.isStreamUri() : false; - }); + final state = await controller.mopidyService.getPlaybackState(); + final position = await controller.mopidyService.getTimePosition(); + if (mounted) { + setState(() { + playingTlId = + state != null && (state == PlaybackState.playing || state == PlaybackState.paused) ? tlTrack?.tlid : null; + playbackState = state ?? playbackState; + timePosition = position ?? 0; + streamTitle = strTitle; + isStream = tlTrack != null ? tlTrack.track.uri.isStreamUri() : false; + }); + } } catch (e) { Globals.logger.e(e); + } finally { + controller.mopidyService.setBusy(false); } } // updates current track view, playback state und position within track. void updateTrackPlayback() async { try { - TrackPlaybackInfo? info = mopidyService.trackPlaybackNotifier.value; + TrackPlaybackInfo? info = controller.mopidyService.trackPlaybackNotifier.value; if (info != null) { var state = PlaybackState.stopped; switch (info.state) { @@ -189,7 +192,7 @@ class _TrackListState extends State { void updateStreamTitle() { setState(() { - streamTitle = mopidyService.streamTitleChangedNotifier.value; + streamTitle = controller.mopidyService.streamTitleChangedNotifier.value; }); } @@ -207,10 +210,10 @@ class _TrackListState extends State { updatePlayback(); }); - mopidyService.tracklistChangedNotifier.addListener(updateTracks); - mopidyService.trackPlaybackNotifier.addListener(updateTrackPlayback); - mopidyService.playbackStateNotifier.addListener(updatePlayback); - mopidyService.streamTitleChangedNotifier.addListener(updateStreamTitle); + controller.mopidyService.tracklistChangedNotifier.addListener(updateTracks); + controller.mopidyService.trackPlaybackNotifier.addListener(updateTrackPlayback); + controller.mopidyService.playbackStateNotifier.addListener(updatePlayback); + controller.mopidyService.streamTitleChangedNotifier.addListener(updateStreamTitle); controller.selectionModeChanged.addListener(updateSelection); controller.selectionChanged.addListener(updateSelection); controller.splitEnabled.addListener(updateSplitMode); @@ -235,14 +238,14 @@ class _TrackListState extends State { @override void dispose() { - refreshSubscription?.cancel(); - mopidyService.tracklistChangedNotifier.removeListener(updateTracks); - mopidyService.trackPlaybackNotifier.removeListener(updateTrackPlayback); - mopidyService.playbackStateNotifier.removeListener(updatePlayback); - mopidyService.streamTitleChangedNotifier.removeListener(updateStreamTitle); + controller.mopidyService.tracklistChangedNotifier.removeListener(updateTracks); + controller.mopidyService.trackPlaybackNotifier.removeListener(updateTrackPlayback); + controller.mopidyService.playbackStateNotifier.removeListener(updatePlayback); + controller.mopidyService.streamTitleChangedNotifier.removeListener(updateStreamTitle); controller.selectionModeChanged.removeListener(updateSelection); controller.selectionChanged.removeListener(updateSelection); controller.splitEnabled.removeListener(updateSplitMode); + refreshSubscription?.cancel(); super.dispose(); } @@ -251,9 +254,9 @@ class _TrackListState extends State { void itemMovedCb(int start, int current) async { try { if (start < current) { - await mopidyService.move(start, current - 1); + await controller.mopidyService.move(start, current - 1); } else { - await mopidyService.move(start, current); + await controller.mopidyService.move(start, current); } } catch (e) { Globals.logger.e(e); @@ -264,7 +267,7 @@ class _TrackListState extends State { var r = await showActionDialog([ItemActionOption.play, ItemActionOption.addToPlaylist]); switch (r) { case ItemActionOption.play: - mopidyService.play(track.asRef); + controller.mopidyService.play(track.asRef); break; case ItemActionOption.addToPlaylist: if (context.mounted) { @@ -290,7 +293,7 @@ class _TrackListState extends State { return; } // store the position in current track - timePosition = (await mopidyService.getTimePosition()) ?? timePosition; + timePosition = (await controller.mopidyService.getTimePosition()) ?? timePosition; controller.splitEnabled.value = !splitEnabled; } catch (e) { Globals.logger.e(e); @@ -309,7 +312,7 @@ class _TrackListState extends State { onPressed: () async { try { // store the position in current track - timePosition = (await mopidyService.getTimePosition()) ?? timePosition; + timePosition = (await controller.mopidyService.getTimePosition()) ?? timePosition; controller.splitEnabled.value = !splitEnabled; } catch (e) { Globals.logger.e(e); @@ -348,29 +351,28 @@ class _TrackListState extends State { var children = splitEnabled ? [Expanded(child: listView), currentlyPlayingPanel] : [currentlyPlayingPanel]; - return BusyWrapper( - Scaffold( - appBar: AppBar( - title: Text(S.of(context).trackListPageTitle), - centerTitle: true, - leading: ActionButton(Icons.arrow_back, - valueListenable: controller.selectionChanged, () { - controller.notifyUnselect(); - }), - actions: [ - ActionButton( - Icons.delete, valueListenable: controller.selectionChanged, controller.deleteSelectedTracks), - ActionButton(Icons.playlist_add, () async { - var selectedItems = await controller.getSelectedItems(); - if (context.mounted) { - await controller.addItemsToPlaylist(context, selectedItems); - } - controller.notifyUnselect(); - }, valueListenable: controller.selectionChanged), - VolumeControl(), - TracklistAppBarMenu(controller) - ]), - body: MaterialPageFrame(child: Column(mainAxisSize: MainAxisSize.max, children: children))), - showBusy); + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).trackListPageTitle), + centerTitle: true, + leading: + ActionButton(Icons.arrow_back, valueListenable: controller.selectionChanged, () { + controller.notifyUnselect(); + }), + actions: [ + ActionButton( + Icons.delete, valueListenable: controller.selectionChanged, controller.deleteSelectedTracks), + ActionButton(Icons.playlist_add, () async { + var selectedItems = await controller.getSelectedItems(); + if (context.mounted) { + await controller.addItemsToPlaylist(context, selectedItems); + } + controller.notifyUnselect(); + }, valueListenable: controller.selectionChanged), + VolumeControl(), + TracklistAppBarMenu(controller) + ]), + body: MaterialPageFrame(child: Column(mainAxisSize: MainAxisSize.max, children: children)), + ); } } diff --git a/lib/routes/application_routes.dart b/lib/routes/application_routes.dart index 1e625bc..be11844 100644 --- a/lib/routes/application_routes.dart +++ b/lib/routes/application_routes.dart @@ -30,7 +30,6 @@ import 'package:mopicon/pages/browse/library_browser_controller.dart'; import 'package:mopicon/pages/playlist/playlist_page.dart'; import 'package:mopicon/pages/settings/preferences_page.dart'; import 'package:mopicon/pages/about/about_page.dart'; -import 'package:mopicon/pages/connecting_screen/connecting_screen.dart'; import 'package:mopicon/pages/tracklist/tracklist_view_controller.dart'; import 'package:mopicon/pages/search/search_page.dart'; import 'package:mopicon/pages/search/search_view_controller.dart'; @@ -58,8 +57,6 @@ class ApplicationRoutes { static final ApplicationRoutes _instance = ApplicationRoutes._privateConstructor(); - final _mopidyService = GetIt.instance(); - factory ApplicationRoutes() { return _instance; } @@ -69,32 +66,16 @@ class ApplicationRoutes { navigatorKey: rootNavigatorKey, initialLocation: tracksPath, debugLogDiagnostics: true, - // redirect to the login page if the user is not logged in - redirect: (BuildContext context, GoRouterState state) async { - if (state.matchedLocation == settingsPath) { - return null; - } - - final bool connected = _mopidyService.connected; - if (!connected) { - return connectingPath; - } - - if (connected && state.matchedLocation == connectingPath) { - return tracksPath; - } - - // no need to redirect at all - return null; - }, routes: [ - GoRoute( - name: connecting, - path: connectingPath, - builder: (BuildContext context, GoRouterState state) { + /** + GoRoute( + name: connecting, + path: connectingPath, + builder: (BuildContext context, GoRouterState state) { return const ConnectingScreen(); - }, - ), + }, + ), + */ GoRoute( path: settingsPath, builder: (BuildContext context, GoRouterState state) { @@ -192,15 +173,6 @@ class ApplicationRoutes { GoRouter.of(rootNavigatorKey.currentContext!).goNamed(tracks, queryParameters: {'title': 'Tracks'}); } - void gotoConnecting([int? maxRetries]) { - if (maxRetries != null) { - GoRouter.of(rootNavigatorKey.currentContext!) - .goNamed(connecting, queryParameters: {'maxRetries': maxRetries.toString()}); - } else { - GoRouter.of(rootNavigatorKey.currentContext!).goNamed(connecting); - } - } - void gotoSettings() { GoRouter.of(rootNavigatorKey.currentContext!).go(settingsPath); } diff --git a/lib/services/cover_service.dart b/lib/services/cover_service.dart index 629917e..c16320d 100644 --- a/lib/services/cover_service.dart +++ b/lib/services/cover_service.dart @@ -21,8 +21,8 @@ */ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:mopicon/utils/logging_utils.dart'; import 'mopidy_service.dart'; -import 'package:mopicon/utils/cache.dart'; import 'package:mopicon/utils/image_utils.dart'; import 'package:mopicon/pages/settings/preferences_controller.dart'; import 'package:mopicon/extensions/mopidy_utils.dart'; @@ -37,9 +37,6 @@ class CoverServiceImpl extends CoverService { final _mopidyService = GetIt.instance(); final _preferences = GetIt.instance(); - // cache Image objects returned from mopidy. - final _mImages = Cache(500, 3000); - @override Future getImage(String uri) async { Widget? image = await _getImage(uri); @@ -53,18 +50,22 @@ class CoverServiceImpl extends CoverService { Future _getImage(String? uri) async { if (uri != null) { - var mImage = _mImages.get(uri); - if (mImage == null) { + try { + var mImage = ImageUtils.noIcon; Map> images = await _mopidyService.getImages([uri]); if (images[uri] != null && images[uri]!.isNotEmpty) { mImage = images[uri]!.first; - _mImages.put(uri, mImage); } - } - if (mImage != null) { - // images loaded from network are internally cached - Image img = Image.network(_preferences.computeNetworkUrl(mImage)); + Image img = Image.network( + _preferences.computeNetworkUrl(mImage), + errorBuilder: (BuildContext context, Object obj, StackTrace? st) { + logger.e(obj.toString()); + return ImageUtils.noIcon; + }, + ); return Future.value(img); + } catch (e, s) { + logger.e(e, stackTrace: s); } } return Future.value(null); diff --git a/lib/services/mopidy_service.dart b/lib/services/mopidy_service.dart index 4f7232e..9251748 100644 --- a/lib/services/mopidy_service.dart +++ b/lib/services/mopidy_service.dart @@ -22,6 +22,7 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show BuildContext, ValueNotifier; import 'package:mopicon/components/error_snackbar.dart'; import 'package:mopicon/extensions/mopidy_utils.dart'; @@ -43,6 +44,11 @@ abstract class MopidyService { /// Notification about current connection state. Stream get connectionState$; + /// Notification about operation state + Stream get busyState$; + + void setBusy(bool busy); + /// Notifier if items were added or removed from the tracklist. ValueNotifier> get tracklistChangedNotifier; @@ -70,6 +76,8 @@ abstract class MopidyService { Future connect(String uri, {int? maxRetries}); + Future waitConnected(); + void stop(); bool get connected; @@ -86,7 +94,7 @@ abstract class MopidyService { Future> search(SearchCriteria criteria); - Future>> getImages(List albumUris); + Future>> getImages(List albumUris); /// Returns a flattened list for [items]. /// @@ -175,6 +183,9 @@ class MopidyServiceImpl extends MopidyService { /// Notification about current connection state. final _connectionState$ = PublishSubject(); + /// Notification about current operation state. + final _busyState$ = PublishSubject(); + /// Notifier if items were added or removed from the tracklist. final _tracklistChangedNotifier = ValueNotifier>([]); @@ -197,6 +208,9 @@ class MopidyServiceImpl extends MopidyService { /// Notification if track was added to or deleted from a playlist. final _playlistChangedNotifier = ValueNotifier(null); + @override + Stream get busyState$ => _busyState$.stream; + @override Stream get connectionState$ => _connectionState$.stream; @@ -243,7 +257,6 @@ class MopidyServiceImpl extends MopidyService { }); */ _mopidy.clientState$.listen((value) { - _connected = false; switch (value.state) { case ClientState.online: _stopped = false; @@ -251,11 +264,16 @@ class MopidyServiceImpl extends MopidyService { _connectionState$.add(MopidyConnectionState.online); break; case ClientState.offline: + _connected = false; _connectionState$.add(MopidyConnectionState.offline); break; case ClientState.reconnecting: + _connected = false; _connectionState$.add(MopidyConnectionState.reconnecting); break; + case ClientState.reconnectionPending: + _connected = false; + break; default: break; } @@ -294,9 +312,27 @@ class MopidyServiceImpl extends MopidyService { }); } + @override + void setBusy(bool busy) { + _busyState$.add(busy); + } + + @override + Future waitConnected() { + if (!_connected) { + return connectionState$.firstWhere((MopidyConnectionState info) { + return info == MopidyConnectionState.online; + }); + } else { + return Future.value(MopidyConnectionState.online); + } + } + @override Future> getUriSchemes() { - return _mopidy.getUriSchemes(); + return waitConnected().then((_) { + return _mopidy.getUriSchemes(); + }); } @override @@ -322,161 +358,198 @@ class MopidyServiceImpl extends MopidyService { @override Future> browse(Ref? parent) async { - var refs = await _mopidy.library.browse(parent?.uri); - // lookup and add album extra info - List uris = refs.map((e) => e.type == Ref.typeAlbum ? e.uri : null).nonNulls.toList(); - - if (uris.isNotEmpty) { - Map> trackMap = await _mopidy.library.lookup(uris); - if (trackMap.isNotEmpty) { - for (var ref in refs) { - var tracks = trackMap[ref.uri]; - if (tracks != null && tracks.isNotEmpty) { - Album? album = tracks.first.album; - if (album != null) { - ref.extraData = AlbumInfoExtraData(album); + return waitConnected().then((_) async { + var refs = await _mopidy.library.browse(parent?.uri); + // lookup and add album extra info + List uris = refs.map((e) => e.type == Ref.typeAlbum ? e.uri : null).nonNulls.toList(); + + if (uris.isNotEmpty) { + Map> trackMap = await _mopidy.library.lookup(uris); + if (trackMap.isNotEmpty) { + for (var ref in refs) { + var tracks = trackMap[ref.uri]; + if (tracks != null && tracks.isNotEmpty) { + Album? album = tracks.first.album; + if (album != null) { + ref.extraData = AlbumInfoExtraData(album); + } } } } } - } - return refs; + return refs; + }); } @override Future> flatten(List items, {Ref? playlist}) async { - assert(items is List || items is List || items is List); - - try { - if (items is List) { - List result = List.empty(growable: true); - for (Ref track in (items as List)) { - if (track.type == Ref.typeAlbum || track.type == Ref.typeDirectory) { - final children = await browse(track); - final List trx = children.map((e) => e.type == Ref.typeTrack ? e : null).toList().nonNulls.toList(); - result.addAll(trx); - } else if (track.type == Ref.typePlaylist) { - List children = await getPlaylistItems(track); - final List trx = children.map((e) => e.asRef).toList(); - result.addAll(trx); - } else if (track.type == Ref.typeTrack) { - result.add(track); + return waitConnected().then((_) async { + assert(items is List || items is List || items is List); + + try { + if (items is List) { + List result = List.empty(growable: true); + for (Ref track in (items as List)) { + if (track.type == Ref.typeAlbum || track.type == Ref.typeDirectory) { + final children = await browse(track); + final List trx = children.map((e) => e.type == Ref.typeTrack ? e : null).toList().nonNulls.toList(); + result.addAll(trx); + } else if (track.type == Ref.typePlaylist) { + List children = await getPlaylistItems(track); + final List trx = children.map((e) => e.asRef).toList(); + result.addAll(trx); + } else if (track.type == Ref.typeTrack) { + result.add(track); + } } + return result as List; + } else if (items is List) { + List result = List.empty(growable: true); + result.addAll(items as List); + return result as List; + } else { + List result = List.empty(growable: true); + result.addAll(items as List); + return result as List; } - return result as List; - } else if (items is List) { - List result = List.empty(growable: true); - result.addAll(items as List); - return result as List; - } else { - List result = List.empty(growable: true); - result.addAll(items as List); - return result as List; + } catch (e, s) { + Globals.logger.e(e, stackTrace: s); } - } catch (e, s) { - Globals.logger.e(e, stackTrace: s); - } - return []; + return []; + }); } @override Future> search(SearchCriteria criteria) { - return _mopidy.library.search(criteria, null, false); + return waitConnected().then((_) { + return _mopidy.library.search(criteria, null, false); + }); } @override - Future>> getImages(List albumUris) { - return _mopidy.library.getImages(albumUris); + Future>> getImages(List albumUris) { + return waitConnected().then((_) { + return _mopidy.library.getImages(albumUris); + }); } @override Future> getTracklistTlTracks() { - return _mopidy.tracklist.getTlTracks(); + return waitConnected().then((_) { + return _mopidy.tracklist.getTlTracks(); + }); } @override Future> addTracksToTracklist(List tracks) async { assert(tracks is List || tracks is List || tracks is List); - var uris = List.empty(growable: true); - for (var track in tracks) { - String uri = getUri(track)!; - uris.add(uri); - } - return _mopidy.tracklist.add(uris, null); + return waitConnected().then((_) { + var uris = List.empty(growable: true); + for (var track in tracks) { + String uri = getUri(track)!; + uris.add(uri); + } + return _mopidy.tracklist.add(uris, null); + }); } @override Future getTracklistLength() { - return _mopidy.tracklist.getLength(); + return waitConnected().then((_) { + return _mopidy.tracklist.getLength(); + }); } @override Future move(int from, int to) { - return _mopidy.tracklist.move(from, from, to); + return waitConnected().then((_) { + return _mopidy.tracklist.move(from, from, to); + }); } @override Future clearTracklist() { - return _mopidy.tracklist.clear(); + return waitConnected().then((_) { + return _mopidy.tracklist.clear(); + }); } @override Future deleteFromTracklist(List tlids) async { if (tlids.isNotEmpty) { - await _mopidy.tracklist.remove(FilterCriteria().tlid([...tlids]).toMap()); + return waitConnected().then((_) async { + await _mopidy.tracklist.remove(FilterCriteria().tlid([...tlids]).toMap()); + return Future.value(null); + }); } return Future.value(null); } @override Future> addTrackToTracklist(T track) { - return addTracksToTracklist([track]); + return waitConnected().then((_) { + return addTracksToTracklist([track]); + }); } @override Future playback(PlaybackAction action, int? tlId) { - switch (action) { - case PlaybackAction.stop: - return _mopidy.playback.stop(); - case PlaybackAction.play: - return tlId != null ? _mopidy.playback.play(tlId) : Future.value(null); - case PlaybackAction.pause: - return _mopidy.playback.pause(); - case PlaybackAction.resume: - return _mopidy.playback.resume(); - } + return waitConnected().then((_) { + switch (action) { + case PlaybackAction.stop: + return _mopidy.playback.stop(); + case PlaybackAction.play: + return tlId != null ? _mopidy.playback.play(tlId) : Future.value(null); + case PlaybackAction.pause: + return _mopidy.playback.pause(); + case PlaybackAction.resume: + return _mopidy.playback.resume(); + } + }); } @override Future playNext() { - return _mopidy.playback.next(); + return waitConnected().then((_) { + return _mopidy.playback.next(); + }); } @override Future playPrevious() { - return _mopidy.playback.previous(); + return waitConnected().then((_) { + return _mopidy.playback.previous(); + }); } @override Future getCurrentTlTrack() { - return _mopidy.playback.getCurrentTlTrack(); + return waitConnected().then((_) { + return _mopidy.playback.getCurrentTlTrack(); + }); } @override Future getPreviousTlid() { - return _mopidy.tracklist.getPreviousTlid(); + return waitConnected().then((_) { + return _mopidy.tracklist.getPreviousTlid(); + }); } @override Future getNextTlid() { - return _mopidy.tracklist.getNextTlid(); + return waitConnected().then((_) { + return _mopidy.tracklist.getNextTlid(); + }); } @override Future getLastTrackId(Ref track) async { - List tracklist = await getTracklistTlTracks(); - return findIdForTrack(tracklist, track); + return waitConnected().then((_) async { + List tracklist = await getTracklistTlTracks(); + return findIdForTrack(tracklist, track); + }); } static int findIdForTrack(List tracklist, Ref track) { @@ -486,181 +559,217 @@ class MopidyServiceImpl extends MopidyService { @override Future getTimePosition() { - return _mopidy.playback.getTimePosition(); + return waitConnected().then((_) { + return _mopidy.playback.getTimePosition(); + }); } @override Future getPlaybackState() { - return _mopidy.playback.getState(); + return waitConnected().then((_) { + return _mopidy.playback.getState(); + }); } @override Future seek(int timePosition) { - return _mopidy.playback.seek(timePosition); + return waitConnected().then((_) { + return _mopidy.playback.seek(timePosition); + }); } @override Future getStreamTitle() { - return _mopidy.playback.getStreamTitle(); + return waitConnected().then((_) { + return _mopidy.playback.getStreamTitle(); + }); } @override Future play(Ref track) async { - int tlid = await getLastTrackId(track); - if (tlid == -1) { - List tl = await addTrackToTracklist(track); - tlid = findIdForTrack(tl, track); - } - if (tlid != -1) { - return playback(PlaybackAction.play, tlid); - } + return waitConnected().then((_) async { + int tlid = await getLastTrackId(track); + if (tlid == -1) { + List tl = await addTrackToTracklist(track); + tlid = findIdForTrack(tl, track); + } + if (tlid != -1) { + return playback(PlaybackAction.play, tlid); + } + }); } // Volume control and muting. @override Future isMuted() { - return _mopidy.mixer.getMute(); + return waitConnected().then((_) { + return _mopidy.mixer.getMute(); + }); } @override Future setMute(bool mute) { - return _mopidy.mixer.setMute(mute); + return waitConnected().then((_) { + return _mopidy.mixer.setMute(mute); + }); } @override Future setVolume(int volume) { - return _mopidy.mixer.setVolume(volume); + return waitConnected().then((_) { + return _mopidy.mixer.setVolume(volume); + }); } @override Future getVolume() { - return _mopidy.mixer.getVolume(); + return waitConnected().then((_) { + return _mopidy.mixer.getVolume(); + }); } // Playlists @override Future> getPlaylists() { - return _mopidy.playlists.asList(); + return waitConnected().then((_) { + return _mopidy.playlists.asList(); + }); } @override Future> getPlaylistItems(Ref playlist) async { assert(playlist.type == Ref.typePlaylist); - List result = []; - Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); - if (pl != null) { - for (var track in pl.tracks) { - if (!track.uri.isStreamUri()) { - Map> trackMap = await _mopidy.library.lookup([track.uri]); - trackMap[track.uri] != null ? result.add(trackMap[track.uri]!.first) : null; - } else { - result.add(track); + return waitConnected().then((_) async { + List result = []; + Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); + if (pl != null) { + for (var track in pl.tracks) { + if (!track.uri.isStreamUri()) { + Map> trackMap = await _mopidy.library.lookup([track.uri]); + trackMap[track.uri] != null ? result.add(trackMap[track.uri]!.first) : null; + } else { + result.add(track); + } } } - } - return Future.value(result); + return Future.value(result); + }); } @override Future createPlaylist(String name) async { - List lists = await _mopidy.playlists.asList(); - if (!lists.map((e) => e.name == name).contains(true)) { - final pl = await _mopidy.playlists.create(name, null); - lists.add(Ref(pl.uri, pl.name, Ref.typePlaylist)); - await _mopidy.playlists.refresh(null); - playlistsChangedNotifier.value = lists; - return Future.value(pl); - } else { - return Future.value(null); - } + return waitConnected().then((_) async { + List lists = await _mopidy.playlists.asList(); + if (!lists.map((e) => e.name == name).contains(true)) { + final pl = await _mopidy.playlists.create(name, null); + lists.add(Ref(pl.uri, pl.name, Ref.typePlaylist)); + await _mopidy.playlists.refresh(null); + playlistsChangedNotifier.value = lists; + return Future.value(pl); + } else { + return Future.value(null); + } + }); } @override Future savePlaylist(Playlist playlist) { - return _mopidy.playlists.save(playlist); + return waitConnected().then((_) { + return _mopidy.playlists.save(playlist); + }); } @override Future deletePlaylist(Ref playlist) { assert(playlist.type == Ref.typePlaylist); - return _mopidy.playlists.delete(playlist.uri); + return waitConnected().then((_) { + return _mopidy.playlists.delete(playlist.uri); + }); } @override Future addToPlaylist(BuildContext context, Ref playlist, List items) async { assert(items is List || items is List || items is List); - Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); - bool trackAdded = false; - if (pl != null) { - for (var item in items) { - if (item is Ref) { - Track tr = (await _mopidy.library.lookup([item.uri])).values.first[0]; - // Special error handling if this is a stream uri and lookup fails if the stream is invalid - // or cannot be accessed. Mopidy dart client API sets 'INVALID_STREAM_ERROR' as the name. - if (tr.name != 'INVALID_STREAM_ERROR') { - pl.addTrack(tr); + return waitConnected().then((_) async { + Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); + bool trackAdded = false; + if (pl != null) { + for (var item in items) { + if (item is Ref) { + Track tr = (await _mopidy.library.lookup([item.uri])).values.first[0]; + // Special error handling if this is a stream uri and lookup fails if the stream is invalid + // or cannot be accessed. Mopidy dart client API sets 'INVALID_STREAM_ERROR' as the name. + if (tr.name != 'INVALID_STREAM_ERROR') { + pl.addTrack(tr); + trackAdded = true; + } else { + if (context.mounted) { + showError(S.of(context).newStreamAccessError, tr.uri); + } + } + } else if (item is TlTrack) { + pl.addTrack(item.track); trackAdded = true; } else { - if (context.mounted) { - showError(S.of(context).newStreamAccessError, tr.uri); - } + pl.addTrack(item as Track); + trackAdded = true; } - } else if (item is TlTrack) { - pl.addTrack(item.track); - trackAdded = true; - } else { - pl.addTrack(item as Track); - trackAdded = true; + } + if (trackAdded) { + Playlist? result = await _mopidy.playlists.save(pl); + return Future.value(result); } } - if (trackAdded) { - Playlist? result = await _mopidy.playlists.save(pl); - return Future.value(result); - } - } - return Future.value(null); + return Future.value(null); + }); } @override Future movePlaylistItem(Ref playlist, int from, int to) async { - Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); - if (pl != null) { - if (to >= 0 && to < pl.tracks.length) { - Track t = pl.tracks.removeAt(from); - pl.tracks.insert(to, t); - Playlist? result = await _mopidy.playlists.save(pl); - return Future.value(result); + return waitConnected().then((_) async { + Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); + if (pl != null) { + if (to >= 0 && to < pl.tracks.length) { + Track t = pl.tracks.removeAt(from); + pl.tracks.insert(to, t); + Playlist? result = await _mopidy.playlists.save(pl); + return Future.value(result); + } } - } - return Future.value(null); + return Future.value(null); + }); } @override Future deletePlaylistItems(Ref playlist, SelectedItemPositions positions) async { - Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); - if (pl != null) { - var remaining = positions.removeSelected(pl.tracks); - pl.tracks.clear(); - pl.tracks.addAll(remaining); - Playlist? result = await _mopidy.playlists.save(pl); - return Future.value(result); - } - return Future.value(null); + return waitConnected().then((_) async { + Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); + if (pl != null) { + var remaining = positions.removeSelected(pl.tracks); + pl.tracks.clear(); + pl.tracks.addAll(remaining); + Playlist? result = await _mopidy.playlists.save(pl); + return Future.value(result); + } + return Future.value(null); + }); } @override Future renamePlaylist(Ref playlist, String name) async { - Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); - if (pl != null) { - pl.name = name; - await _mopidy.playlists.save(pl); - await _mopidy.playlists.delete(pl.uri); - return true; - } - return false; + return waitConnected().then((_) async { + Playlist? pl = await _mopidy.playlists.lookup(playlist.uri); + if (pl != null) { + pl.name = name; + await _mopidy.playlists.save(pl); + await _mopidy.playlists.delete(pl.uri); + return true; + } + return false; + }); } } diff --git a/lib/utils/open_value_notifier.dart b/lib/utils/open_value_notifier.dart index e7ca35f..0bf581b 100644 --- a/lib/utils/open_value_notifier.dart +++ b/lib/utils/open_value_notifier.dart @@ -24,7 +24,7 @@ import 'package:flutter/material.dart' show ValueNotifier; // A ValueNotifier which allows to unconditionally trigger // its listeners with notify(). class OpenValueNotifier extends ValueNotifier { - OpenValueNotifier(value) : super(value); + OpenValueNotifier(super.value); void notify() => notifyListeners(); }