mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/routes.gr.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/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/browse/sections.dart';
|
import 'package:spotube/provider/metadata_plugin/browse/sections.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/common.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:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
import 'package:flutter_undraw/flutter_undraw.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(
|
return SliverInfiniteList(
|
||||||
hasReachedMax: browseSections.asData?.value.hasMore == false,
|
hasReachedMax: browseSections.asData?.value.hasMore == false,
|
||||||
isLoading: !browseSections.isLoading && browseSections.isLoadingNextPage,
|
isLoading: !browseSections.isLoading && browseSections.isLoadingNextPage,
|
||||||
|
@ -80,6 +80,9 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
for (final tile in sidebarTileList)
|
for (final tile in sidebarTileList)
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
|
style: router.currentPath.startsWith(tile.pathPrefix)
|
||||||
|
? const ButtonStyle.secondary()
|
||||||
|
: null,
|
||||||
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
tooltip: TooltipContainer(child: Text(tile.title)).call,
|
tooltip: TooltipContainer(child: Text(tile.title)).call,
|
||||||
@ -94,6 +97,9 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
NavigationLabel(child: Text(context.l10n.library)),
|
NavigationLabel(child: Text(context.l10n.library)),
|
||||||
for (final tile in sidebarLibraryTileList)
|
for (final tile in sidebarLibraryTileList)
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
|
style: router.currentPath.startsWith(tile.pathPrefix)
|
||||||
|
? const ButtonStyle.secondary()
|
||||||
|
: null,
|
||||||
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.navigateTo(tile.route);
|
context.navigateTo(tile.route);
|
||||||
|
@ -104,15 +104,15 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Button.secondary(
|
Button.primary(
|
||||||
leading: const Icon(SpotubeIcons.anonymous),
|
leading: const Icon(SpotubeIcons.extensions),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await KVStoreService.setDoneGettingStarted(true);
|
await KVStoreService.setDoneGettingStarted(true);
|
||||||
if (context.mounted) {
|
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:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.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/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.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/core/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class UserAlbumsPage extends HookConsumerWidget {
|
class UserAlbumsPage extends HookConsumerWidget {
|
||||||
@ -50,10 +53,27 @@ class UserAlbumsPage extends HookConsumerWidget {
|
|||||||
[];
|
[];
|
||||||
}, [albumsQuery.asData?.value, searchText.value]);
|
}, [albumsQuery.asData?.value, searchText.value]);
|
||||||
|
|
||||||
|
if (albumsQuery.error
|
||||||
|
case MetadataPluginException(
|
||||||
|
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||||
|
message: _,
|
||||||
|
)) {
|
||||||
|
return const Center(child: NoDefaultMetadataPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
if (authenticated.asData?.value != true) {
|
if (authenticated.asData?.value != true) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (albumsQuery.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: albumsQuery.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSavedAlbumsProvider);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
@ -12,6 +12,8 @@ import 'package:spotube/collections/fake.dart';
|
|||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/fallbacks/anonymous_fallback.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/modules/artist/artist_card.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/waypoint.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/core/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class UserArtistsPage extends HookConsumerWidget {
|
class UserArtistsPage extends HookConsumerWidget {
|
||||||
@ -55,10 +58,27 @@ class UserArtistsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
|
if (artistQuery.error
|
||||||
|
case MetadataPluginException(
|
||||||
|
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||||
|
message: _,
|
||||||
|
)) {
|
||||||
|
return const Center(child: NoDefaultMetadataPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
if (authenticated.asData?.value != true) {
|
if (authenticated.asData?.value != true) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (artistQuery.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: artistQuery.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSavedArtistsProvider);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
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/assets.gen.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.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/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.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/library/playlists.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class UserPlaylistsPage extends HookConsumerWidget {
|
class UserPlaylistsPage extends HookConsumerWidget {
|
||||||
@ -78,10 +81,27 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
|
if (playlistsQuery.error
|
||||||
|
case MetadataPluginException(
|
||||||
|
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||||
|
message: _,
|
||||||
|
)) {
|
||||||
|
return const Center(child: NoDefaultMetadataPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
if (authenticated.asData?.value != true) {
|
if (authenticated.asData?.value != true) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playlistsQuery.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: playlistsQuery.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSavedPlaylistsProvider);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return material.RefreshIndicator.adaptive(
|
return material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(metadataPluginSavedPlaylistsProvider);
|
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/routes.gr.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.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/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/string.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/artists.dart';
|
||||||
import 'package:spotube/pages/search/tabs/playlists.dart';
|
import 'package:spotube/pages/search/tabs/playlists.dart';
|
||||||
import 'package:spotube/pages/search/tabs/tracks.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/provider/metadata_plugin/search/all.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
|
|
||||||
final searchTermStateProvider = StateProvider<String>((ref) {
|
final searchTermStateProvider = StateProvider<String>((ref) {
|
||||||
return "";
|
return "";
|
||||||
@ -37,8 +38,6 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
final controller = useShadcnTextEditingController();
|
final controller = useShadcnTextEditingController();
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
|
|
||||||
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
|
||||||
|
|
||||||
final searchTerm = ref.watch(searchTermStateProvider);
|
final searchTerm = ref.watch(searchTermStateProvider);
|
||||||
final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider);
|
final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider);
|
||||||
final selectedChip = useState<String?>(
|
final selectedChip = useState<String?>(
|
||||||
@ -83,147 +82,163 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
if (kTitlebarVisible)
|
if (kTitlebarVisible)
|
||||||
const TitleBar(automaticallyImplyLeading: false, height: 30)
|
const TitleBar(automaticallyImplyLeading: false, height: 30)
|
||||||
],
|
],
|
||||||
child: authenticated.asData?.value != true
|
child: Builder(builder: (context) {
|
||||||
? const AnonymousFallback()
|
if (searchChipSnapshot.error
|
||||||
: Column(
|
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,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
Expanded(
|
horizontal: 20,
|
||||||
child: Padding(
|
vertical: 10,
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 20,
|
child: ListenableBuilder(
|
||||||
vertical: 10,
|
listenable: controller,
|
||||||
),
|
builder: (context, _) {
|
||||||
child: ListenableBuilder(
|
final suggestions = controller.text.isEmpty
|
||||||
listenable: controller,
|
? KVStoreService.recentSearches
|
||||||
builder: (context, _) {
|
: KVStoreService.recentSearches
|
||||||
final suggestions = controller.text.isEmpty
|
.where(
|
||||||
? KVStoreService.recentSearches
|
(s) =>
|
||||||
: KVStoreService.recentSearches
|
weightedRatio(
|
||||||
.where(
|
s.toLowerCase(),
|
||||||
(s) =>
|
controller.text.toLowerCase(),
|
||||||
weightedRatio(
|
) >
|
||||||
s.toLowerCase(),
|
50,
|
||||||
controller.text.toLowerCase(),
|
)
|
||||||
) >
|
.toList();
|
||||||
50,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return KeyboardListener(
|
return KeyboardListener(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: (value) {
|
||||||
|
final isEnter = value.logicalKey ==
|
||||||
|
LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
|
if (isEnter) {
|
||||||
|
onSubmitted(controller.text);
|
||||||
|
focusNode.unfocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AutoComplete(
|
||||||
|
suggestions: suggestions.length <= 2
|
||||||
|
? [
|
||||||
|
...suggestions,
|
||||||
|
"Twenty One Pilots",
|
||||||
|
"Linkin Park",
|
||||||
|
"d4vd"
|
||||||
|
]
|
||||||
|
: suggestions,
|
||||||
|
completer: (suggestion) => suggestion,
|
||||||
|
mode: AutoCompleteMode.replaceAll,
|
||||||
|
child: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onKeyEvent: (value) {
|
controller: controller,
|
||||||
final isEnter = value.logicalKey ==
|
features: [
|
||||||
LogicalKeyboardKey.enter;
|
const InputFeature.leading(
|
||||||
|
Icon(SpotubeIcons.search),
|
||||||
if (isEnter) {
|
),
|
||||||
onSubmitted(controller.text);
|
InputFeature.trailing(
|
||||||
focusNode.unfocus();
|
AnimatedCrossFade(
|
||||||
}
|
duration:
|
||||||
},
|
const Duration(milliseconds: 300),
|
||||||
child: AutoComplete(
|
crossFadeState:
|
||||||
suggestions: suggestions.length <= 2
|
controller.text.isNotEmpty
|
||||||
? [
|
|
||||||
...suggestions,
|
|
||||||
"Twenty One Pilots",
|
|
||||||
"Linkin Park",
|
|
||||||
"d4vd"
|
|
||||||
]
|
|
||||||
: suggestions,
|
|
||||||
completer: (suggestion) => suggestion,
|
|
||||||
mode: AutoCompleteMode.replaceAll,
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: controller,
|
|
||||||
features: [
|
|
||||||
const InputFeature.leading(
|
|
||||||
Icon(SpotubeIcons.search),
|
|
||||||
),
|
|
||||||
InputFeature.trailing(
|
|
||||||
AnimatedCrossFade(
|
|
||||||
duration: const Duration(
|
|
||||||
milliseconds: 300),
|
|
||||||
crossFadeState: controller
|
|
||||||
.text.isNotEmpty
|
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: IconButton.ghost(
|
firstChild: IconButton.ghost(
|
||||||
size: ButtonSize.small,
|
size: ButtonSize.small,
|
||||||
icon: const Icon(
|
icon:
|
||||||
SpotubeIcons.close),
|
const Icon(SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.clear();
|
controller.clear();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
secondChild:
|
secondChild: const SizedBox.square(
|
||||||
const SizedBox.square(
|
dimension: 28),
|
||||||
dimension: 28),
|
),
|
||||||
),
|
)
|
||||||
)
|
],
|
||||||
],
|
textInputAction: TextInputAction.search,
|
||||||
textInputAction: TextInputAction.search,
|
placeholder: Text(context.l10n.search),
|
||||||
placeholder: Text(context.l10n.search),
|
onSubmitted: onSubmitted,
|
||||||
onSubmitted: onSubmitted,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
const Gap(12),
|
|
||||||
if (searchChipSnapshot.asData?.value != null)
|
|
||||||
for (final chip in searchChipSnapshot.asData!.value)
|
|
||||||
Chip(
|
|
||||||
style: selectedChip.value == chip
|
|
||||||
? ButtonVariance.primary.copyWith(
|
|
||||||
decoration: (context, states, value) {
|
|
||||||
return ButtonVariance.primary
|
|
||||||
.decoration(context, states)
|
|
||||||
.copyWithIfBoxDecoration(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(100),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: ButtonVariance.secondary.copyWith(
|
|
||||||
decoration: (context, states, value) {
|
|
||||||
return ButtonVariance.secondary
|
|
||||||
.decoration(context, states)
|
|
||||||
.copyWithIfBoxDecoration(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(100),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: Text(chip.capitalize()),
|
|
||||||
onPressed: () {
|
|
||||||
selectedChip.value = chip;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
child: switch (selectedChip.value) {
|
|
||||||
"tracks" => const SearchPageTracksTab(),
|
|
||||||
"albums" => const SearchPageAlbumsTab(),
|
|
||||||
"artists" => const SearchPageArtistsTab(),
|
|
||||||
"playlists" => const SearchPagePlaylistsTab(),
|
|
||||||
_ => const SearchPageAllTab(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
const Gap(12),
|
||||||
|
if (searchChipSnapshot.asData?.value != null)
|
||||||
|
for (final chip in searchChipSnapshot.asData!.value)
|
||||||
|
Chip(
|
||||||
|
style: selectedChip.value == chip
|
||||||
|
? ButtonVariance.primary.copyWith(
|
||||||
|
decoration: (context, states, value) {
|
||||||
|
return ButtonVariance.primary
|
||||||
|
.decoration(context, states)
|
||||||
|
.copyWithIfBoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(100),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: ButtonVariance.secondary.copyWith(
|
||||||
|
decoration: (context, states, value) {
|
||||||
|
return ButtonVariance.secondary
|
||||||
|
.decoration(context, states)
|
||||||
|
.copyWithIfBoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(100),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: Text(chip.capitalize()),
|
||||||
|
onPressed: () {
|
||||||
|
selectedChip.value = chip;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: switch (selectedChip.value) {
|
||||||
|
"tracks" => const SearchPageTracksTab(),
|
||||||
|
"albums" => const SearchPageAlbumsTab(),
|
||||||
|
"artists" => const SearchPageArtistsTab(),
|
||||||
|
"playlists" => const SearchPagePlaylistsTab(),
|
||||||
|
_ => const SearchPageAllTab(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/fake.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/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/modules/search/loading.dart';
|
import 'package:spotube/modules/search/loading.dart';
|
||||||
@ -23,6 +24,15 @@ class SearchPageAlbumsTab extends HookConsumerWidget {
|
|||||||
final searchAlbums =
|
final searchAlbums =
|
||||||
searchAlbumsSnapshot.asData?.value.items ?? [FakeData.albumSimple];
|
searchAlbumsSnapshot.asData?.value.items ?? [FakeData.albumSimple];
|
||||||
|
|
||||||
|
if (searchAlbumsSnapshot.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: searchAlbumsSnapshot.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSearchAlbumsProvider(searchTerm));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SearchPlaceholder(
|
return SearchPlaceholder(
|
||||||
snapshot: searchAlbumsSnapshot,
|
snapshot: searchAlbumsSnapshot,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.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/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/modules/search/loading.dart';
|
import 'package:spotube/modules/search/loading.dart';
|
||||||
import 'package:spotube/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.dart';
|
||||||
@ -19,6 +20,15 @@ class SearchPageAllTab extends HookConsumerWidget {
|
|||||||
final searchSnapshot =
|
final searchSnapshot =
|
||||||
ref.watch(metadataPluginSearchAllProvider(searchTerm));
|
ref.watch(metadataPluginSearchAllProvider(searchTerm));
|
||||||
|
|
||||||
|
if (searchSnapshot.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: searchSnapshot.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSearchAllProvider(searchTerm));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SearchPlaceholder(
|
return SearchPlaceholder(
|
||||||
snapshot: searchSnapshot,
|
snapshot: searchSnapshot,
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
|
@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
|
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||||
import 'package:spotube/components/waypoint.dart';
|
import 'package:spotube/components/waypoint.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
@ -27,6 +28,15 @@ class SearchPageArtistsTab extends HookConsumerWidget {
|
|||||||
ref.read(metadataPluginSearchArtistsProvider(searchTerm).notifier);
|
ref.read(metadataPluginSearchArtistsProvider(searchTerm).notifier);
|
||||||
final searchArtists = searchArtistsSnapshot.asData?.value.items ?? [];
|
final searchArtists = searchArtistsSnapshot.asData?.value.items ?? [];
|
||||||
|
|
||||||
|
if (searchArtistsSnapshot.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: searchArtistsSnapshot.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSearchArtistsProvider(searchTerm));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SearchPlaceholder(
|
return SearchPlaceholder(
|
||||||
snapshot: searchArtistsSnapshot,
|
snapshot: searchArtistsSnapshot,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/fake.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/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/modules/search/loading.dart';
|
import 'package:spotube/modules/search/loading.dart';
|
||||||
@ -23,6 +24,15 @@ class SearchPagePlaylistsTab extends HookConsumerWidget {
|
|||||||
final searchPlaylists = searchPlaylistsSnapshot.asData?.value.items ??
|
final searchPlaylists = searchPlaylistsSnapshot.asData?.value.items ??
|
||||||
[FakeData.playlistSimple];
|
[FakeData.playlistSimple];
|
||||||
|
|
||||||
|
if (searchPlaylistsSnapshot.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: searchPlaylistsSnapshot.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSearchPlaylistsProvider(searchTerm));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SearchPlaceholder(
|
return SearchPlaceholder(
|
||||||
snapshot: searchPlaylistsSnapshot,
|
snapshot: searchPlaylistsSnapshot,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -4,6 +4,7 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_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/components/track_tile/track_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
@ -31,6 +32,15 @@ class SearchPageTracksTab extends HookConsumerWidget {
|
|||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
|
|
||||||
|
if (searchTracksSnapshot.hasError) {
|
||||||
|
return ErrorBox(
|
||||||
|
error: searchTracksSnapshot.error!,
|
||||||
|
onRetry: () {
|
||||||
|
ref.invalidate(metadataPluginSearchTracksProvider(searchTerm));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SearchPlaceholder(
|
return SearchPlaceholder(
|
||||||
snapshot: searchTracksSnapshot,
|
snapshot: searchTracksSnapshot,
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/common.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 =
|
final metadataPluginAlbumProvider =
|
||||||
FutureProvider.autoDispose.family<SpotubeFullAlbumObject, String>(
|
FutureProvider.autoDispose.family<SpotubeFullAlbumObject, String>(
|
||||||
@ -12,9 +12,7 @@ final metadataPluginAlbumProvider =
|
|||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No metadata plugin is not set",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.album.getAlbum(id);
|
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/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/common.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 =
|
final metadataPluginArtistProvider =
|
||||||
FutureProvider.autoDispose.family<SpotubeFullArtistObject, String>(
|
FutureProvider.autoDispose.family<SpotubeFullArtistObject, String>(
|
||||||
@ -12,9 +12,7 @@ final metadataPluginArtistProvider =
|
|||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No metadata plugin is not set",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.artist.getArtist(artistId);
|
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/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.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
|
class MetadataPluginSavedPlaylistsNotifier
|
||||||
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
|
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
|
||||||
@ -111,9 +111,7 @@ final metadataPluginIsSavedPlaylistProvider =
|
|||||||
final plugin = await ref.watch(metadataPluginProvider.future);
|
final plugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (plugin == null) {
|
if (plugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"Failed to get metadata plugin",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final follows = await plugin.user.isSavedPlaylist(id);
|
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/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/utils/common.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';
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
|
||||||
class MetadataPluginPlaylistNotifier
|
class MetadataPluginPlaylistNotifier
|
||||||
@ -13,9 +13,7 @@ class MetadataPluginPlaylistNotifier
|
|||||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"Metadata plugin is not set",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin;
|
return metadataPlugin;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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 =
|
final metadataPluginSearchAllProvider =
|
||||||
FutureProvider.autoDispose.family<SpotubeSearchResponseObject, String>(
|
FutureProvider.autoDispose.family<SpotubeSearchResponseObject, String>(
|
||||||
@ -9,9 +9,7 @@ final metadataPluginSearchAllProvider =
|
|||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No default metadata plugin found",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.search.all(query);
|
return metadataPlugin.search.all(query);
|
||||||
@ -22,9 +20,7 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
|
|||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No default metadata plugin found",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return metadataPlugin.search.chips;
|
return metadataPlugin.search.chips;
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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 =
|
final metadataPluginTrackProvider =
|
||||||
FutureProvider.family<SpotubeFullTrackObject, String>((ref, trackId) async {
|
FutureProvider.family<SpotubeFullTrackObject, String>((ref, trackId) async {
|
||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No metadata plugin is set as default.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.track.getTrack(trackId);
|
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/models/metadata/metadata.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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';
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
|
||||||
extension PaginationExtension<T> on AsyncValue<T> {
|
extension PaginationExtension<T> on AsyncValue<T> {
|
||||||
@ -20,8 +20,7 @@ mixin MetadataPluginMixin<K>
|
|||||||
final plugin = await ref.read(metadataPluginProvider.future);
|
final plugin = await ref.read(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (plugin == null) {
|
if (plugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"Metadata plugin is not set");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin;
|
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/playlists.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
enum TrackOptionValue {
|
enum TrackOptionValue {
|
||||||
@ -97,9 +97,7 @@ class TrackOptionsActions {
|
|||||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null) {
|
if (metadataPlugin == null) {
|
||||||
throw MetadataPluginException.noDefaultPlugin(
|
throw MetadataPluginException.noDefaultPlugin();
|
||||||
"No default metadata plugin set",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final tracks = await metadataPlugin.track.radio(track.id);
|
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,
|
pluginDownloadFailed,
|
||||||
duplicatePlugin,
|
duplicatePlugin,
|
||||||
pluginByteCodeFileNotFound,
|
pluginByteCodeFileNotFound,
|
||||||
|
noDefaultPlugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataPluginException implements Exception {
|
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.',
|
'Plugin byte code file, plugin.out not found. Please ensure the plugin is correctly packaged.',
|
||||||
errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound,
|
errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound,
|
||||||
);
|
);
|
||||||
|
MetadataPluginException.noDefaultPlugin()
|
||||||
|
: this._(
|
||||||
|
'No default metadata plugin is set. Please set a default plugin in the settings.',
|
||||||
|
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'MetadataPluginException: $message';
|
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/empty.svg
|
||||||
- packages/flutter_undraw/assets/undraw/no_data.svg
|
- packages/flutter_undraw/assets/undraw/no_data.svg
|
||||||
- packages/flutter_undraw/assets/undraw/process.svg
|
- packages/flutter_undraw/assets/undraw/process.svg
|
||||||
|
- packages/flutter_undraw/assets/undraw/stars.svg
|
||||||
# hetu script bytecode
|
# hetu script bytecode
|
||||||
- packages/hetu_std/assets/bytecode/std.out
|
- packages/hetu_std/assets/bytecode/std.out
|
||||||
- packages/hetu_otp_util/assets/bytecode/otp_util.out
|
- packages/hetu_otp_util/assets/bytecode/otp_util.out
|
||||||
@ -232,6 +233,20 @@ flutter:
|
|||||||
- asset: assets/fonts/Cookie-Regular.ttf
|
- asset: assets/fonts/Cookie-Regular.ttf
|
||||||
style: normal
|
style: normal
|
||||||
weight: 500
|
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:
|
flutter_gen:
|
||||||
output: lib/collections
|
output: lib/collections
|
||||||
|
Loading…
Reference in New Issue
Block a user