spotube/lib/pages/library/user_artists.dart
Kingkor Roy Tirtho 7037145519 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.
2025-08-19 22:34:37 +06:00

198 lines
7.6 KiB
Dart

import 'package:flutter/material.dart' as material;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
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/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';
import 'package:spotube/extensions/constrains.dart';
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 {
static const name = 'user_artists';
const UserArtistsPage({super.key});
@override
Widget build(BuildContext context, ref) {
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
final artistQuery = ref.watch(metadataPluginSavedArtistsProvider);
final artistQueryNotifier =
ref.watch(metadataPluginSavedArtistsProvider.notifier);
final searchText = useState('');
final filteredArtists = useMemoized(() {
final artists = artistQuery.asData?.value.items ?? [];
if (searchText.value.isEmpty) {
return artists.toList();
}
return artists
.map((e) => (
weightedRatio(e.name, searchText.value),
e,
))
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
}, [artistQuery.asData?.value.items, searchText.value]);
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(
child: material.RefreshIndicator.adaptive(
onRefresh: () async {
ref.invalidate(metadataPluginSavedArtistsProvider);
},
child: InterScrollbar(
controller: controller,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomScrollView(
controller: controller,
slivers: [
SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).colorScheme.background,
floating: true,
flexibleSpace: SizedBox(
height: 48,
child: TextField(
onChanged: (value) => searchText.value = value,
features: const [
InputFeature.leading(Icon(SpotubeIcons.filter)),
],
placeholder: Text(context.l10n.filter_artist),
),
),
),
const SliverGap(10),
if (filteredArtists.isNotEmpty || artistQuery.isLoading)
SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder(
itemCount: filteredArtists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: constrains.smAndDown ? 225 : 250,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
if (filteredArtists.isNotEmpty &&
index == filteredArtists.length) {
if (artistQuery.asData?.value.hasMore != true) {
return const SizedBox.shrink();
}
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: artistQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: ArtistCard(FakeData.artist),
),
);
}
return Skeletonizer(
enabled: artistQuery.isLoading,
child: ArtistCard(
filteredArtists.elementAtOrNull(index) ??
FakeData.artist,
),
);
},
);
})
else if (filteredArtists.isEmpty &&
searchText.value.isEmpty &&
!artistQuery.isLoading)
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.followMeDrone,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.not_following_artists,
textAlign: TextAlign.center,
).muted().small()
],
),
)
else
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.taken,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.nothing_found,
textAlign: TextAlign.center,
).muted().small()
],
),
),
const SliverSafeArea(sliver: SliverGap(10)),
],
),
),
),
),
),
);
}
}