mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 16:25:16 +00:00
feat: black list artist or track
This commit is contained in:
parent
f79223cd41
commit
947c14353e
@ -1,27 +1,35 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide Colors;
|
import 'package:fluent_ui/fluent_ui.dart' hide Colors;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:platform_ui/platform_ui.dart';
|
import 'package:platform_ui/platform_ui.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/hover_builder.dart';
|
import 'package:spotube/components/shared/hover_builder.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/hooks/use_platform_property.dart';
|
import 'package:spotube/hooks/use_platform_property.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class ArtistCard extends HookWidget {
|
class ArtistCard extends HookConsumerWidget {
|
||||||
final Artist artist;
|
final Artist artist;
|
||||||
const ArtistCard(this.artist, {Key? key}) : super(key: key);
|
const ArtistCard(this.artist, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
final backgroundImage = UniversalImage.imageProvider(
|
final backgroundImage = UniversalImage.imageProvider(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
TypeConversionUtils.image_X_UrlString(
|
||||||
artist.images,
|
artist.images,
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final isBlackListed = ref.watch(
|
||||||
|
BlackListNotifier.provider.select(
|
||||||
|
(blacklist) => blacklist.contains(
|
||||||
|
BlacklistedElement.artist(artist.id!, artist.name!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
final boxShadow = usePlatformProperty<BoxShadow?>(
|
final boxShadow = usePlatformProperty<BoxShadow?>(
|
||||||
(context) => PlatformProperty(
|
(context) => PlatformProperty(
|
||||||
android: BoxShadow(
|
android: BoxShadow(
|
||||||
@ -78,14 +86,19 @@ class ArtistCard extends HookWidget {
|
|||||||
boxShadow: [
|
boxShadow: [
|
||||||
if (boxShadow != null) boxShadow,
|
if (boxShadow != null) boxShadow,
|
||||||
],
|
],
|
||||||
border: [TargetPlatform.windows, TargetPlatform.macOS]
|
border: isBlackListed
|
||||||
.contains(platform)
|
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: PlatformTheme.of(context).borderColor ??
|
color: Colors.red[400]!,
|
||||||
Colors.transparent,
|
width: 2,
|
||||||
width: 1,
|
|
||||||
)
|
)
|
||||||
: null,
|
: [TargetPlatform.windows, TargetPlatform.macOS]
|
||||||
|
.contains(platform)
|
||||||
|
? Border.all(
|
||||||
|
color: PlatformTheme.of(context).borderColor ??
|
||||||
|
Colors.transparent,
|
||||||
|
width: 1,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
|
75
lib/components/settings/blacklist_dialog.dart
Normal file
75
lib/components/settings/blacklist_dialog.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:platform_ui/platform_ui.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
class BlackListDialog extends HookConsumerWidget {
|
||||||
|
const BlackListDialog({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||||
|
final searchText = useState("");
|
||||||
|
|
||||||
|
final filteredBlacklist = useMemoized(
|
||||||
|
() {
|
||||||
|
if (searchText.value.isEmpty) {
|
||||||
|
return blacklist;
|
||||||
|
}
|
||||||
|
return blacklist
|
||||||
|
.map((e) => Tuple2(
|
||||||
|
weightedRatio("${e.name} ${e.type.name}", searchText.value),
|
||||||
|
e,
|
||||||
|
))
|
||||||
|
.sorted((a, b) => b.item1.compareTo(a.item1))
|
||||||
|
.where((e) => e.item1 > 50)
|
||||||
|
.map((e) => e.item2)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
[blacklist, searchText.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return PlatformAlertDialog(
|
||||||
|
title: const PlatformText("Blacklist"),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: PlatformTextField(
|
||||||
|
onChanged: (value) => searchText.value = value,
|
||||||
|
placeholder: "Search",
|
||||||
|
prefixIcon: Icons.search_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: filteredBlacklist.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = filteredBlacklist.elementAt(index);
|
||||||
|
return ListTile(
|
||||||
|
leading: PlatformText("${index + 1}."),
|
||||||
|
minLeadingWidth: 20,
|
||||||
|
title: PlatformText("${item.name} (${item.type.name})"),
|
||||||
|
subtitle: PlatformText.caption(item.id),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: Icon(Icons.delete_forever_rounded,
|
||||||
|
color: Colors.red[400]),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(BlackListNotifier.provider.notifier)
|
||||||
|
.remove(filteredBlacklist.elementAt(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -9,12 +9,14 @@ class Action extends StatelessWidget {
|
|||||||
final Widget icon;
|
final Widget icon;
|
||||||
final void Function() onPressed;
|
final void Function() onPressed;
|
||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
|
final Color? backgroundColor;
|
||||||
const Action({
|
const Action({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.isExpanded = true,
|
this.isExpanded = true,
|
||||||
|
this.backgroundColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,6 +25,7 @@ class Action extends StatelessWidget {
|
|||||||
return PlatformIconButton(
|
return PlatformIconButton(
|
||||||
icon: icon,
|
icon: icon,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
tooltip: text is PlatformText
|
tooltip: text is PlatformText
|
||||||
? (text as PlatformText).data
|
? (text as PlatformText).data
|
||||||
: text.toStringShallow().split(",").last.replaceAll(
|
: text.toStringShallow().split(",").last.replaceAll(
|
||||||
@ -31,18 +34,42 @@ class Action extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return PlatformTextButton(
|
if (backgroundColor == null) {
|
||||||
style: TextButton.styleFrom(
|
return PlatformTextButton(
|
||||||
foregroundColor: PlatformTextTheme.of(context).body?.color,
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.all(20),
|
foregroundColor:
|
||||||
),
|
backgroundColor ?? PlatformTextTheme.of(context).body?.color,
|
||||||
onPressed: onPressed,
|
backgroundColor: backgroundColor,
|
||||||
child: Row(
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
),
|
||||||
icon,
|
onPressed: onPressed,
|
||||||
const SizedBox(width: 10),
|
child: Row(
|
||||||
text,
|
children: [
|
||||||
],
|
icon,
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
text,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: PlatformFilledButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor:
|
||||||
|
backgroundColor ?? PlatformTextTheme.of(context).body?.color,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
text,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -75,7 +102,7 @@ class AdaptiveActions extends HookWidget {
|
|||||||
children: actions
|
children: actions
|
||||||
.map(
|
.map(
|
||||||
(action) => SizedBox(
|
(action) => SizedBox(
|
||||||
width: 200,
|
width: 250,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: action),
|
Expanded(child: action),
|
||||||
@ -99,6 +126,7 @@ class AdaptiveActions extends HookWidget {
|
|||||||
icon: action.icon,
|
icon: action.icon,
|
||||||
onPressed: action.onPressed,
|
onPressed: action.onPressed,
|
||||||
text: action.text,
|
text: action.text,
|
||||||
|
backgroundColor: action.backgroundColor,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
@ -15,6 +15,7 @@ import 'package:spotube/components/root/sidebar.dart';
|
|||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/auth_provider.dart';
|
import 'package:spotube/provider/auth_provider.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/playback_provider.dart';
|
import 'package:spotube/provider/playback_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/mutations/mutations.dart';
|
import 'package:spotube/services/mutations/mutations.dart';
|
||||||
@ -62,6 +63,13 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
final isBlackListed = ref.watch(
|
||||||
|
BlackListNotifier.provider.select(
|
||||||
|
(blacklist) => blacklist.contains(
|
||||||
|
BlacklistedElement.track(track.value.id!, track.value.name!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
final removingTrack = useState<String?>(null);
|
final removingTrack = useState<String?>(null);
|
||||||
@ -179,9 +187,11 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive
|
color: isBlackListed
|
||||||
? Theme.of(context).popupMenuTheme.color
|
? Colors.red[100]
|
||||||
: Colors.transparent,
|
: isActive
|
||||||
|
? Theme.of(context).popupMenuTheme.color
|
||||||
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(isActive ? 10 : 0),
|
borderRadius: BorderRadius.circular(isActive ? 10 : 0),
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
@ -238,22 +248,43 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
backgroundColor: PlatformTheme.of(context).primaryColor,
|
backgroundColor: PlatformTheme.of(context).primaryColor,
|
||||||
hoverColor:
|
hoverColor:
|
||||||
PlatformTheme.of(context).primaryColor?.withOpacity(0.5),
|
PlatformTheme.of(context).primaryColor?.withOpacity(0.5),
|
||||||
onPressed: () => onTrackPlayButtonPressed?.call(
|
onPressed: !isBlackListed
|
||||||
track.value,
|
? () => onTrackPlayButtonPressed?.call(
|
||||||
),
|
track.value,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
PlatformText(
|
Row(
|
||||||
track.value.name ?? "",
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
fontSize: breakpoint.isSm ? 14 : 17,
|
Flexible(
|
||||||
),
|
child: PlatformText(
|
||||||
overflow: TextOverflow.ellipsis,
|
track.value.name ?? "",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: breakpoint.isSm ? 14 : 17,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isBlackListed) ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
PlatformText(
|
||||||
|
"Blacklisted",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
),
|
),
|
||||||
isLocal
|
isLocal
|
||||||
? PlatformText(
|
? PlatformText(
|
||||||
@ -327,6 +358,32 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
actionShare(track.value);
|
actionShare(track.value);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
Action(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.playlist_remove_rounded,
|
||||||
|
color: isBlackListed ? Colors.white : Colors.red[400],
|
||||||
|
),
|
||||||
|
backgroundColor: isBlackListed ? Colors.red[400] : null,
|
||||||
|
text: PlatformText(
|
||||||
|
"${isBlackListed ? "Remove from" : "Add to"} blacklist",
|
||||||
|
style: TextStyle(
|
||||||
|
color: isBlackListed ? Colors.white : Colors.red[400],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (isBlackListed) {
|
||||||
|
ref.read(BlackListNotifier.provider.notifier).remove(
|
||||||
|
BlacklistedElement.track(
|
||||||
|
track.value.id!, track.value.name!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ref.read(BlackListNotifier.provider.notifier).add(
|
||||||
|
BlacklistedElement.track(
|
||||||
|
track.value.id!, track.value.name!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,7 @@ import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
|||||||
import 'package:spotube/components/shared/track_table/track_tile.dart';
|
import 'package:spotube/components/shared/track_table/track_tile.dart';
|
||||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
import 'package:spotube/provider/downloader_provider.dart';
|
||||||
import 'package:spotube/provider/playback_provider.dart';
|
import 'package:spotube/provider/playback_provider.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -217,7 +218,17 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
selected.value = [...selected.value, track.value.id!];
|
selected.value = [...selected.value, track.value.id!];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onTrackPlayButtonPressed?.call(track.value);
|
final isBlackListed = ref.read(
|
||||||
|
BlackListNotifier.provider.select(
|
||||||
|
(blacklist) => blacklist.contains(
|
||||||
|
BlacklistedElement.track(
|
||||||
|
track.value.id!, track.value.name!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!isBlackListed) {
|
||||||
|
onTrackPlayButtonPressed?.call(track.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: TrackTile(
|
child: TrackTile(
|
||||||
|
@ -17,6 +17,7 @@ import 'package:spotube/hooks/use_breakpoints.dart';
|
|||||||
import 'package:spotube/models/current_playlist.dart';
|
import 'package:spotube/models/current_playlist.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/auth_provider.dart';
|
import 'package:spotube/provider/auth_provider.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/playback_provider.dart';
|
import 'package:spotube/provider/playback_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
@ -78,6 +79,11 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final data = artistsQuery.data!;
|
final data = artistsQuery.data!;
|
||||||
|
|
||||||
|
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||||
|
final isBlackListed = blacklist.contains(
|
||||||
|
BlacklistedElement.artist(artistId, data.name!),
|
||||||
|
);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: parentScrollController,
|
controller: parentScrollController,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@ -104,15 +110,40 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(
|
mainAxisSize: MainAxisSize.min,
|
||||||
horizontal: 10, vertical: 5),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: Colors.blue,
|
padding: const EdgeInsets.symmetric(
|
||||||
borderRadius: BorderRadius.circular(50)),
|
horizontal: 10, vertical: 5),
|
||||||
child: PlatformText(data.type!.toUpperCase(),
|
decoration: BoxDecoration(
|
||||||
style: chipTextVariant?.copyWith(
|
color: Colors.blue,
|
||||||
color: Colors.white)),
|
borderRadius: BorderRadius.circular(50)),
|
||||||
|
child: PlatformText(
|
||||||
|
data.type!.toUpperCase(),
|
||||||
|
style: chipTextVariant?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isBlackListed) ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[400],
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(50)),
|
||||||
|
child: PlatformText(
|
||||||
|
"Blacklisted",
|
||||||
|
style: chipTextVariant?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
),
|
),
|
||||||
PlatformText(
|
PlatformText(
|
||||||
data.name!,
|
data.name!,
|
||||||
@ -149,6 +180,8 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final queryBowl = QueryBowl.of(context);
|
||||||
|
|
||||||
return PlatformFilledButton(
|
return PlatformFilledButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
@ -162,7 +195,8 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
[artistId],
|
[artistId],
|
||||||
);
|
);
|
||||||
await isFollowingQuery.refetch();
|
await isFollowingQuery.refetch();
|
||||||
QueryBowl.of(context)
|
|
||||||
|
queryBowl
|
||||||
.getInfiniteQuery(
|
.getInfiniteQuery(
|
||||||
Queries.artist.followedByMe
|
Queries.artist.followedByMe
|
||||||
.queryKey,
|
.queryKey,
|
||||||
@ -191,25 +225,55 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
PlatformIconButton(
|
||||||
|
tooltip: "Add to blacklisted artists",
|
||||||
|
icon: Icon(
|
||||||
|
Icons.person_remove_rounded,
|
||||||
|
color: !isBlackListed
|
||||||
|
? Colors.red[400]
|
||||||
|
: Colors.white,
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
isBlackListed ? Colors.red[400] : null,
|
||||||
|
onPressed: () async {
|
||||||
|
if (isBlackListed) {
|
||||||
|
ref
|
||||||
|
.read(BlackListNotifier
|
||||||
|
.provider.notifier)
|
||||||
|
.remove(
|
||||||
|
BlacklistedElement.artist(
|
||||||
|
data.id!, data.name!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(BlackListNotifier
|
||||||
|
.provider.notifier)
|
||||||
|
.add(
|
||||||
|
BlacklistedElement.artist(
|
||||||
|
data.id!, data.name!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
PlatformIconButton(
|
PlatformIconButton(
|
||||||
icon: const Icon(Icons.share_rounded),
|
icon: const Icon(Icons.share_rounded),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: data.externalUrls?.spotify),
|
text: data.externalUrls?.spotify),
|
||||||
).then((val) {
|
);
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
width: 300,
|
width: 300,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
content: PlatformText(
|
content: PlatformText(
|
||||||
"Artist URL copied to clipboard",
|
"Artist URL copied to clipboard",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:platform_ui/platform_ui.dart';
|
import 'package:platform_ui/platform_ui.dart';
|
||||||
|
import 'package:spotube/components/settings/blacklist_dialog.dart';
|
||||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart';
|
import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
@ -12,7 +13,6 @@ import 'package:spotube/hooks/use_breakpoints.dart';
|
|||||||
import 'package:spotube/main.dart';
|
import 'package:spotube/main.dart';
|
||||||
import 'package:spotube/collections/spotify_markets.dart';
|
import 'package:spotube/collections/spotify_markets.dart';
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
|
||||||
import 'package:spotube/provider/auth_provider.dart';
|
import 'package:spotube/provider/auth_provider.dart';
|
||||||
import 'package:spotube/provider/playback_provider.dart';
|
import 'package:spotube/provider/playback_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
@ -337,6 +337,22 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PlatformListTile(
|
||||||
|
leading: const Icon(Icons.playlist_remove_rounded),
|
||||||
|
title: const PlatformText(
|
||||||
|
"Track/Artist Blacklist",
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showPlatformAlertDialog(
|
||||||
|
context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (context) {
|
||||||
|
return const BlackListDialog();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.open_in_new_rounded),
|
||||||
|
),
|
||||||
PlatformText(
|
PlatformText(
|
||||||
" Search",
|
" Search",
|
||||||
style: PlatformTextTheme.of(context)
|
style: PlatformTextTheme.of(context)
|
||||||
|
87
lib/provider/blacklist_provider.dart
Normal file
87
lib/provider/blacklist_provider.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotube/models/current_playlist.dart';
|
||||||
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
|
|
||||||
|
enum BlacklistedType {
|
||||||
|
artist,
|
||||||
|
track;
|
||||||
|
|
||||||
|
static BlacklistedType fromName(String name) =>
|
||||||
|
BlacklistedType.values.firstWhere((e) => e.name == name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlacklistedElement {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final BlacklistedType type;
|
||||||
|
|
||||||
|
BlacklistedElement.artist(this.id, this.name) : type = BlacklistedType.artist;
|
||||||
|
|
||||||
|
BlacklistedElement.track(this.id, this.name) : type = BlacklistedType.track;
|
||||||
|
|
||||||
|
BlacklistedElement.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'],
|
||||||
|
name = json['name'],
|
||||||
|
type = BlacklistedType.fromName(json['type']);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {'id': id, 'type': type.name, 'name': name};
|
||||||
|
|
||||||
|
@override
|
||||||
|
operator ==(other) =>
|
||||||
|
other is BlacklistedElement &&
|
||||||
|
other.id == id &&
|
||||||
|
other.type == type &&
|
||||||
|
other.name == name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode ^ type.hashCode ^ name.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlackListNotifier
|
||||||
|
extends PersistedStateNotifier<Set<BlacklistedElement>> {
|
||||||
|
BlackListNotifier() : super({});
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
StateNotifierProvider<BlackListNotifier, Set<BlacklistedElement>>(
|
||||||
|
(ref) => BlackListNotifier(),
|
||||||
|
);
|
||||||
|
|
||||||
|
void add(BlacklistedElement element) {
|
||||||
|
state = state.union({element});
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(BlacklistedElement element) {
|
||||||
|
state = state.difference({element});
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentPlaylist filterPlaylist(CurrentPlaylist playlist) {
|
||||||
|
return CurrentPlaylist(
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
thumbnail: playlist.thumbnail,
|
||||||
|
tracks: playlist.tracks.where(
|
||||||
|
(track) {
|
||||||
|
return !state
|
||||||
|
.contains(BlacklistedElement.track(track.id!, track.name!)) &&
|
||||||
|
!(track.artists ?? []).any(
|
||||||
|
(artist) => state.contains(
|
||||||
|
BlacklistedElement.artist(artist.id!, artist.name!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<BlacklistedElement> fromJson(Map<String, dynamic> json) {
|
||||||
|
return json['blacklist']
|
||||||
|
.map<BlacklistedElement>((e) => BlacklistedElement.fromJson(e))
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'blacklist': state.map((e) => e.toJson()).toList()};
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ import 'package:spotube/models/current_playlist.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
import 'package:spotube/provider/audio_player_provider.dart';
|
import 'package:spotube/provider/audio_player_provider.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/youtube_provider.dart';
|
import 'package:spotube/provider/youtube_provider.dart';
|
||||||
import 'package:spotube/services/linux_audio_service.dart';
|
import 'package:spotube/services/linux_audio_service.dart';
|
||||||
@ -56,6 +57,9 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
YoutubeExplode youtube;
|
YoutubeExplode youtube;
|
||||||
Ref ref;
|
Ref ref;
|
||||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||||
|
Set<BlacklistedElement> get blacklist => ref.read(BlackListNotifier.provider);
|
||||||
|
BlackListNotifier get blacklistNotifier =>
|
||||||
|
ref.read(BlackListNotifier.provider.notifier);
|
||||||
|
|
||||||
// playlist & track list properties
|
// playlist & track list properties
|
||||||
late LazyBox<CacheTrack> cache;
|
late LazyBox<CacheTrack> cache;
|
||||||
@ -197,7 +201,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
if (index < 0 || index > playlist.tracks.length - 1) return;
|
if (index < 0 || index > playlist.tracks.length - 1) return;
|
||||||
if (isPlaying || status == PlaybackStatus.playing) await stop();
|
if (isPlaying || status == PlaybackStatus.playing) await stop();
|
||||||
this.playlist = playlist;
|
this.playlist = blacklistNotifier.filterPlaylist(playlist);
|
||||||
mobileAudioService?.session?.setActive(true);
|
mobileAudioService?.session?.setActive(true);
|
||||||
final played = this.playlist!.tracks[index];
|
final played = this.playlist!.tracks[index];
|
||||||
status = PlaybackStatus.loading;
|
status = PlaybackStatus.loading;
|
||||||
|
48
lib/utils/persisted_state_notifier.dart
Normal file
48
lib/utils/persisted_state_notifier.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||||
|
get cacheKey => state.runtimeType.toString();
|
||||||
|
|
||||||
|
SharedPreferences? localStorage;
|
||||||
|
|
||||||
|
PersistedStateNotifier(super.state) : super() {
|
||||||
|
SharedPreferences.getInstance().then(
|
||||||
|
(localStorage) {
|
||||||
|
this.localStorage = localStorage;
|
||||||
|
final rawState = localStorage.getString(cacheKey);
|
||||||
|
|
||||||
|
if (rawState != null) {
|
||||||
|
state = fromJson(jsonDecode(rawState));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
T fromJson(Map<String, dynamic> json);
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
@override
|
||||||
|
set state(T value) {
|
||||||
|
if (state == value) return;
|
||||||
|
super.state = value;
|
||||||
|
if (localStorage == null) {
|
||||||
|
SharedPreferences.getInstance().then(
|
||||||
|
(localStorage) {
|
||||||
|
this.localStorage = localStorage;
|
||||||
|
localStorage.setString(
|
||||||
|
cacheKey,
|
||||||
|
jsonEncode(toJson()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
localStorage?.setString(
|
||||||
|
cacheKey,
|
||||||
|
jsonEncode(toJson()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user