feat: add ErrorBox and NoDefaultMetadataPlugin components

- Implemented ErrorBox for displaying error messages with retry functionality and log viewing.
- Created NoDefaultMetadataPlugin to inform users about missing default metadata providers and provide navigation to manage them.
This commit is contained in:
Kingkor Roy Tirtho 2025-08-19 22:34:37 +06:00
parent 08d1c98674
commit 7037145519
30 changed files with 609 additions and 185 deletions

View File

@ -0,0 +1,96 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,137 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
class ErrorBox extends StatelessWidget {
final Object error;
final VoidCallback? onRetry;
const ErrorBox({
super.key,
required this.error,
this.onRetry,
});
@override
Widget build(BuildContext context) {
// Make a monospace error log view. Make sure it's only 4 lines
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
const Basic(
leading: Icon(SpotubeIcons.error),
contentSpacing: 8,
title: Text("An error occurred"),
),
Card(
padding: const EdgeInsets.all(8.0),
filled: true,
fillColor: context.theme.colorScheme.muted,
child: Text(
error.toString(),
style: TextStyle(
// Use monospace
fontFamily: 'Ubuntu Mono',
color: context.theme.colorScheme.mutedForeground,
fontSize: 14,
),
maxLines: 6,
overflow: TextOverflow.ellipsis,
),
),
// Show a dialog with full log and a retry button as well
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Button.text(
leading: const Icon(SpotubeIcons.logs),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 480,
maxHeight:
MediaQuery.of(context).size.height * 0.8,
),
child: AlertDialog(
padding: const EdgeInsets.all(12),
title: Row(
spacing: 8,
children: [
const Icon(SpotubeIcons.logs),
const Text("Logs"),
const Spacer(),
IconButton.ghost(
icon: const Icon(SpotubeIcons.close),
onPressed: () => context.maybePop(),
)
],
),
actions: [
HookBuilder(builder: (context) {
final copied = useState(false);
return Button.ghost(
leading: copied.value
? const Icon(SpotubeIcons.done)
: const Icon(SpotubeIcons.clipboard),
child: const Text("Copy to clipboard"),
onPressed: () {
Clipboard.setData(
ClipboardData(text: error.toString()),
);
copied.value = true;
},
);
})
],
content: SingleChildScrollView(
child: Card(
padding: const EdgeInsets.all(8.0),
filled: true,
fillColor: context.theme.colorScheme.muted,
child: SelectableText(
error.toString(),
style: TextStyle(
// Use monospace
fontFamily: 'Ubuntu Mono',
color: context
.theme.colorScheme.mutedForeground,
fontSize: 16,
),
),
),
),
),
);
},
);
},
child: const Text("View logs"),
),
if (onRetry != null)
Button.text(
leading: const Icon(SpotubeIcons.refresh),
onPressed: onRetry,
child: const Text("Retry"),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
class NoDefaultMetadataPlugin extends StatelessWidget {
const NoDefaultMetadataPlugin({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.stars,
color: context.theme.colorScheme.primary,
),
AutoSizeText(
"You've no default metadata provider set",
style: context.theme.typography.h4,
maxLines: 1,
),
Button.primary(
leading: const Icon(SpotubeIcons.extensions),
child: const Text("Manage metadata providers"),
onPressed: () {
context.pushRoute(const SettingsMetadataProviderRoute());
},
),
],
),
);
}
}

View File

@ -2,10 +2,13 @@ import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/metadata_plugin/browse/sections.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
@ -44,6 +47,29 @@ class HomePageBrowseSection extends HookConsumerWidget {
);
}
if (browseSections.error
case MetadataPluginException(
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
message: _,
)) {
return const SliverFillRemaining(
child: Center(child: NoDefaultMetadataPlugin()),
);
}
if (browseSections.hasError) {
return SliverFillRemaining(
child: Center(
child: ErrorBox(
error: browseSections.error!,
onRetry: () {
ref.invalidate(metadataPluginBrowseSectionsProvider);
},
),
),
);
}
return SliverInfiniteList(
hasReachedMax: browseSections.asData?.value.hasMore == false,
isLoading: !browseSections.isLoading && browseSections.isLoadingNextPage,

View File

@ -80,6 +80,9 @@ class Sidebar extends HookConsumerWidget {
),
for (final tile in sidebarTileList)
NavigationButton(
style: router.currentPath.startsWith(tile.pathPrefix)
? const ButtonStyle.secondary()
: null,
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
child: Tooltip(
tooltip: TooltipContainer(child: Text(tile.title)).call,
@ -94,6 +97,9 @@ class Sidebar extends HookConsumerWidget {
NavigationLabel(child: Text(context.l10n.library)),
for (final tile in sidebarLibraryTileList)
NavigationButton(
style: router.currentPath.startsWith(tile.pathPrefix)
? const ButtonStyle.secondary()
: null,
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
onPressed: () {
context.navigateTo(tile.route);

View File

@ -104,15 +104,15 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Button.secondary(
leading: const Icon(SpotubeIcons.anonymous),
Button.primary(
leading: const Icon(SpotubeIcons.extensions),
onPressed: () async {
await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) {
context.navigateTo(const HomeRoute());
context.pushRoute(const SettingsMetadataProviderRoute());
}
},
child: Text(context.l10n.browse_anonymously),
child: const Text("Install a Metadata Provider"),
),
],
),

View File

@ -9,6 +9,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
@ -17,6 +19,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
import 'package:auto_route/auto_route.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
@RoutePage()
class UserAlbumsPage extends HookConsumerWidget {
@ -50,10 +53,27 @@ class UserAlbumsPage extends HookConsumerWidget {
[];
}, [albumsQuery.asData?.value, searchText.value]);
if (albumsQuery.error
case MetadataPluginException(
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
message: _,
)) {
return const Center(child: NoDefaultMetadataPlugin());
}
if (authenticated.asData?.value != true) {
return const AnonymousFallback();
}
if (albumsQuery.hasError) {
return ErrorBox(
error: albumsQuery.error!,
onRetry: () {
ref.invalidate(metadataPluginSavedAlbumsProvider);
},
);
}
return SafeArea(
bottom: false,
child: Scaffold(

View File

@ -12,6 +12,8 @@ import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
import 'package:spotube/modules/artist/artist_card.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/waypoint.dart';
@ -20,6 +22,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
import 'package:auto_route/auto_route.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
@RoutePage()
class UserArtistsPage extends HookConsumerWidget {
@ -55,10 +58,27 @@ class UserArtistsPage extends HookConsumerWidget {
final controller = useScrollController();
if (artistQuery.error
case MetadataPluginException(
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
message: _,
)) {
return const Center(child: NoDefaultMetadataPlugin());
}
if (authenticated.asData?.value != true) {
return const AnonymousFallback();
}
if (artistQuery.hasError) {
return ErrorBox(
error: artistQuery.error!,
onRetry: () {
ref.invalidate(metadataPluginSavedArtistsProvider);
},
);
}
return SafeArea(
bottom: false,
child: Scaffold(

View File

@ -8,6 +8,8 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
@ -19,6 +21,7 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/core/user.dart';
import 'package:auto_route/auto_route.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
@RoutePage()
class UserPlaylistsPage extends HookConsumerWidget {
@ -78,10 +81,27 @@ class UserPlaylistsPage extends HookConsumerWidget {
final controller = useScrollController();
if (playlistsQuery.error
case MetadataPluginException(
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
message: _,
)) {
return const Center(child: NoDefaultMetadataPlugin());
}
if (authenticated.asData?.value != true) {
return const AnonymousFallback();
}
if (playlistsQuery.hasError) {
return ErrorBox(
error: playlistsQuery.error!,
onRetry: () {
ref.invalidate(metadataPluginSavedPlaylistsProvider);
},
);
}
return material.RefreshIndicator.adaptive(
onRefresh: () async {
ref.invalidate(metadataPluginSavedPlaylistsProvider);

View File

@ -7,7 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/string.dart';
@ -17,10 +18,10 @@ import 'package:spotube/pages/search/tabs/all.dart';
import 'package:spotube/pages/search/tabs/artists.dart';
import 'package:spotube/pages/search/tabs/playlists.dart';
import 'package:spotube/pages/search/tabs/tracks.dart';
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:auto_route/auto_route.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
final searchTermStateProvider = StateProvider<String>((ref) {
return "";
@ -37,8 +38,6 @@ class SearchPage extends HookConsumerWidget {
final controller = useShadcnTextEditingController();
final focusNode = useFocusNode();
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
final searchTerm = ref.watch(searchTermStateProvider);
final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider);
final selectedChip = useState<String?>(
@ -83,9 +82,25 @@ class SearchPage extends HookConsumerWidget {
if (kTitlebarVisible)
const TitleBar(automaticallyImplyLeading: false, height: 30)
],
child: authenticated.asData?.value != true
? const AnonymousFallback()
: Column(
child: Builder(builder: (context) {
if (searchChipSnapshot.error
case MetadataPluginException(
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
message: _
)) {
return const NoDefaultMetadataPlugin();
}
if (searchChipSnapshot.hasError) {
return ErrorBox(
error: searchChipSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchChipsProvider);
},
);
}
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -144,22 +159,21 @@ class SearchPage extends HookConsumerWidget {
),
InputFeature.trailing(
AnimatedCrossFade(
duration: const Duration(
milliseconds: 300),
crossFadeState: controller
.text.isNotEmpty
duration:
const Duration(milliseconds: 300),
crossFadeState:
controller.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon: const Icon(
SpotubeIcons.close),
icon:
const Icon(SpotubeIcons.close),
onPressed: () {
controller.clear();
},
),
secondChild:
const SizedBox.square(
secondChild: const SizedBox.square(
dimension: 28),
),
)
@ -223,7 +237,8 @@ class SearchPage extends HookConsumerWidget {
),
),
],
),
);
}),
),
),
);

View File

@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/modules/search/loading.dart';
@ -23,6 +24,15 @@ class SearchPageAlbumsTab extends HookConsumerWidget {
final searchAlbums =
searchAlbumsSnapshot.asData?.value.items ?? [FakeData.albumSimple];
if (searchAlbumsSnapshot.hasError) {
return ErrorBox(
error: searchAlbumsSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchAlbumsProvider(searchTerm));
},
);
}
return SearchPlaceholder(
snapshot: searchAlbumsSnapshot,
child: Padding(

View File

@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/modules/search/loading.dart';
import 'package:spotube/pages/search/search.dart';
@ -19,6 +20,15 @@ class SearchPageAllTab extends HookConsumerWidget {
final searchSnapshot =
ref.watch(metadataPluginSearchAllProvider(searchTerm));
if (searchSnapshot.hasError) {
return ErrorBox(
error: searchSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchAllProvider(searchTerm));
},
);
}
return SearchPlaceholder(
snapshot: searchSnapshot,
child: InterScrollbar(

View File

@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
@ -27,6 +28,15 @@ class SearchPageArtistsTab extends HookConsumerWidget {
ref.read(metadataPluginSearchArtistsProvider(searchTerm).notifier);
final searchArtists = searchArtistsSnapshot.asData?.value.items ?? [];
if (searchArtistsSnapshot.hasError) {
return ErrorBox(
error: searchArtistsSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchArtistsProvider(searchTerm));
},
);
}
return SearchPlaceholder(
snapshot: searchArtistsSnapshot,
child: AnimatedSwitcher(

View File

@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/modules/search/loading.dart';
@ -23,6 +24,15 @@ class SearchPagePlaylistsTab extends HookConsumerWidget {
final searchPlaylists = searchPlaylistsSnapshot.asData?.value.items ??
[FakeData.playlistSimple];
if (searchPlaylistsSnapshot.hasError) {
return ErrorBox(
error: searchPlaylistsSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchPlaylistsProvider(searchTerm));
},
);
}
return SearchPlaceholder(
snapshot: searchPlaylistsSnapshot,
child: Padding(

View File

@ -4,6 +4,7 @@ import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
@ -31,6 +32,15 @@ class SearchPageTracksTab extends HookConsumerWidget {
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
if (searchTracksSnapshot.hasError) {
return ErrorBox(
error: searchTracksSnapshot.error!,
onRetry: () {
ref.invalidate(metadataPluginSearchTracksProvider(searchTerm));
},
);
}
return SearchPlaceholder(
snapshot: searchTracksSnapshot,
child: InfiniteList(

View File

@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
final metadataPluginAlbumProvider =
FutureProvider.autoDispose.family<SpotubeFullAlbumObject, String>(
@ -12,9 +12,7 @@ final metadataPluginAlbumProvider =
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No metadata plugin is not set",
);
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin.album.getAlbum(id);

View File

@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
final metadataPluginArtistProvider =
FutureProvider.autoDispose.family<SpotubeFullArtistObject, String>(
@ -12,9 +12,7 @@ final metadataPluginArtistProvider =
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No metadata plugin is not set",
);
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin.artist.getArtist(artistId);

View File

@ -4,7 +4,7 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
class MetadataPluginSavedPlaylistsNotifier
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
@ -111,9 +111,7 @@ final metadataPluginIsSavedPlaylistProvider =
final plugin = await ref.watch(metadataPluginProvider.future);
if (plugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"Failed to get metadata plugin",
);
throw MetadataPluginException.noDefaultPlugin();
}
final follows = await plugin.user.isSavedPlaylist(id);

View File

@ -4,7 +4,7 @@ import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/core/user.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:spotube/services/metadata/metadata.dart';
class MetadataPluginPlaylistNotifier
@ -13,9 +13,7 @@ class MetadataPluginPlaylistNotifier
final metadataPlugin = await ref.read(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"Metadata plugin is not set",
);
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin;

View File

@ -1,7 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
final metadataPluginSearchAllProvider =
FutureProvider.autoDispose.family<SpotubeSearchResponseObject, String>(
@ -9,9 +9,7 @@ final metadataPluginSearchAllProvider =
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No default metadata plugin found",
);
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin.search.all(query);
@ -22,9 +20,7 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No default metadata plugin found",
);
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin.search.chips;
});

View File

@ -1,15 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
final metadataPluginTrackProvider =
FutureProvider.family<SpotubeFullTrackObject, String>((ref, trackId) async {
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No metadata plugin is set as default.");
throw MetadataPluginException.noDefaultPlugin();
}
return metadataPlugin.track.getTrack(trackId);

View File

@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:spotube/services/metadata/metadata.dart';
extension PaginationExtension<T> on AsyncValue<T> {
@ -20,8 +20,7 @@ mixin MetadataPluginMixin<K>
final plugin = await ref.read(metadataPluginProvider.future);
if (plugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"Metadata plugin is not set");
throw MetadataPluginException.noDefaultPlugin();
}
return plugin;

View File

@ -20,7 +20,7 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/endpoints/error.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:url_launcher/url_launcher_string.dart';
enum TrackOptionValue {
@ -97,9 +97,7 @@ class TrackOptionsActions {
final metadataPlugin = await ref.read(metadataPluginProvider.future);
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"No default metadata plugin set",
);
throw MetadataPluginException.noDefaultPlugin();
}
final tracks = await metadataPlugin.track.radio(track.id);

View File

@ -1,12 +0,0 @@
class MetadataPluginException implements Exception {
final String exceptionType;
final String message;
MetadataPluginException.noDefaultPlugin(this.message)
: exceptionType = "NoDefault";
@override
String toString() {
return "${exceptionType}MetadataPluginException: $message";
}
}

View File

@ -9,6 +9,7 @@ enum MetadataPluginErrorCode {
pluginDownloadFailed,
duplicatePlugin,
pluginByteCodeFileNotFound,
noDefaultPlugin,
}
class MetadataPluginException implements Exception {
@ -67,6 +68,11 @@ class MetadataPluginException implements Exception {
'Plugin byte code file, plugin.out not found. Please ensure the plugin is correctly packaged.',
errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound,
);
MetadataPluginException.noDefaultPlugin()
: this._(
'No default metadata plugin is set. Please set a default plugin in the settings.',
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
);
@override
String toString() => 'MetadataPluginException: $message';

View File

@ -216,6 +216,7 @@ flutter:
- packages/flutter_undraw/assets/undraw/empty.svg
- packages/flutter_undraw/assets/undraw/no_data.svg
- packages/flutter_undraw/assets/undraw/process.svg
- packages/flutter_undraw/assets/undraw/stars.svg
# hetu script bytecode
- packages/hetu_std/assets/bytecode/std.out
- packages/hetu_otp_util/assets/bytecode/otp_util.out
@ -232,6 +233,20 @@ flutter:
- asset: assets/fonts/Cookie-Regular.ttf
style: normal
weight: 500
- family: Ubuntu Mono
fonts:
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
style: normal
weight: 400
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
style: normal
weight: 700
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
style: italic
weight: 400
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
style: italic
weight: 700
flutter_gen:
output: lib/collections