mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
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:
parent
08d1c98674
commit
7037145519
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal file
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal 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.
|
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
Binary file not shown.
137
lib/components/fallbacks/error_box.dart
Normal file
137
lib/components/fallbacks/error_box.dart
Normal 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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
41
lib/components/fallbacks/no_default_metadata_plugin.dart
Normal file
41
lib/components/fallbacks/no_default_metadata_plugin.dart
Normal 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());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
15
pubspec.yaml
15
pubspec.yaml
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user