Merge branch 'dev' into feat-jiosaavn

This commit is contained in:
Kingkor Roy Tirtho 2023-11-14 18:33:49 +06:00
commit 956a57b3f9
22 changed files with 640 additions and 665 deletions

View File

@ -163,6 +163,8 @@ class UserLocalTracks extends HookConsumerWidget {
final searchFocus = useFocusNode(); final searchFocus = useFocusNode();
final isFiltering = useState(false); final isFiltering = useState(false);
final controller = useScrollController();
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -256,7 +258,9 @@ class UserLocalTracks extends HookConsumerWidget {
ref.refresh(localTracksProvider); ref.refresh(localTracksProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length, itemCount: filteredTracks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
@ -81,6 +82,8 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: playlistsQuery.refresh, onRefresh: playlistsQuery.refresh,
child: SafeArea( child: SafeArea(
child: InterScrollbar(
controller: controller,
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,
slivers: [ slivers: [
@ -145,6 +148,7 @@ class UserPlaylists extends HookConsumerWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -44,6 +44,7 @@ class PlayerQueue extends HookConsumerWidget {
topRight: Radius.circular(10), topRight: Radius.circular(10),
); );
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color; final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized( final filteredTracks = useMemoized(
@ -108,8 +109,7 @@ class PlayerQueue extends HookConsumerWidget {
searchText.value = ''; searchText.value = '';
} }
}, },
child: LayoutBuilder(builder: (context, constraints) { child: Column(
return Column(
children: [ children: [
if (!floating) if (!floating)
Container( Container(
@ -125,7 +125,7 @@ class PlayerQueue extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (constraints.mdAndUp || !isSearching.value) ...[ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
context.l10n.tracks_in_queue(tracks.length), context.l10n.tracks_in_queue(tracks.length),
@ -137,7 +137,7 @@ class PlayerQueue extends HookConsumerWidget {
), ),
const Spacer(), const Spacer(),
], ],
if (constraints.mdAndUp || isSearching.value) if (mediaQuery.mdAndUp || isSearching.value)
TextField( TextField(
onChanged: (value) { onChanged: (value) {
searchText.value = value; searchText.value = value;
@ -145,7 +145,7 @@ class PlayerQueue extends HookConsumerWidget {
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.search, hintText: context.l10n.search,
isDense: true, isDense: true,
prefixIcon: constraints.smAndDown prefixIcon: mediaQuery.smAndDown
? IconButton( ? IconButton(
icon: const Icon( icon: const Icon(
Icons.arrow_back_ios_new_outlined, Icons.arrow_back_ios_new_outlined,
@ -162,8 +162,8 @@ class PlayerQueue extends HookConsumerWidget {
: const Icon(SpotubeIcons.filter), : const Icon(SpotubeIcons.filter),
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 40, maxHeight: 40,
maxWidth: constraints.smAndDown maxWidth: mediaQuery.smAndDown
? constraints.maxWidth - 20 ? mediaQuery.size.width - 40
: 300, : 300,
), ),
), ),
@ -175,14 +175,13 @@ class PlayerQueue extends HookConsumerWidget {
isSearching.value = !isSearching.value; isSearching.value = !isSearching.value;
}, },
), ),
if (constraints.mdAndUp || !isSearching.value) ...[ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
FilledButton( FilledButton(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: backgroundColor:
theme.scaffoldBackgroundColor.withOpacity(0.5), theme.scaffoldBackgroundColor.withOpacity(0.5),
foregroundColor: foregroundColor: theme.textTheme.headlineSmall?.color,
theme.textTheme.headlineSmall?.color,
), ),
child: Row( child: Row(
children: [ children: [
@ -203,8 +202,6 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty) if (!isSearching.value && searchText.value.isEmpty)
Flexible( Flexible(
child: InterScrollbar(
controller: controller,
child: ReorderableListView.builder( child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) { onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex); playlistNotifier.moveTrack(oldIndex, newIndex);
@ -234,8 +231,7 @@ class PlayerQueue extends HookConsumerWidget {
leadingActions: [ leadingActions: [
ReorderableDragStartListener( ReorderableDragStartListener(
index: i, index: i,
child: child: const Icon(SpotubeIcons.dragHandle),
const Icon(SpotubeIcons.dragHandle),
), ),
], ],
), ),
@ -243,12 +239,13 @@ class PlayerQueue extends HookConsumerWidget {
); );
}, },
), ),
),
) )
else else
Flexible( Flexible(
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: filteredTracks.length, itemCount: filteredTracks.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i); final track = filteredTracks.elementAt(i);
@ -271,8 +268,7 @@ class PlayerQueue extends HookConsumerWidget {
), ),
), ),
], ],
); ),
}),
), ),
), ),
), ),

View File

@ -56,7 +56,9 @@ class SiblingTracksSheet extends HookConsumerWidget {
useValueListenable(searchController).text, useValueListenable(searchController).text,
); );
final searchRequest = useMemoized<Future<List<SourceInfo>>>(() async { final controller = useScrollController();
final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty) { if (searchTerm.trim().isEmpty) {
return <SourceInfo>[]; return <SourceInfo>[];
} }
@ -219,8 +221,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
transitionBuilder: (child, animation) => transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: switch (isSearching.value) { child: switch (isSearching.value) {
false => ListView.builder( false => ListView.builder(
controller: controller,
itemCount: siblings.length, itemCount: siblings.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
itemBuilder(siblings[index]), itemBuilder(siblings[index]),
@ -238,7 +242,9 @@ class SiblingTracksSheet extends HookConsumerWidget {
} }
return InterScrollbar( return InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]), itemBuilder(snapshot.data![index]),

View File

@ -49,6 +49,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final scheme = preferences.accentColorScheme; final scheme = preferences.accentColorScheme;
final active = useState<String>(colorsMap.firstWhere( final active = useState<String>(colorsMap.firstWhere(
(element) { (element) {
@ -57,7 +58,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
).name); ).name);
onOk() { onOk() {
preferences.setAccentColorScheme( preferencesNotifier.setAccentColorScheme(
colorsMap.firstWhere( colorsMap.firstWhere(
(element) { (element) {
return element.name == active.value; return element.name == active.value;

View File

@ -1,29 +1,16 @@
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
class InterScrollbar extends HookWidget { class InterScrollbar extends HookWidget {
final Widget child; final Widget child;
final ScrollController? controller; final ScrollController controller;
final bool? thumbVisibility;
final bool? trackVisibility;
final double? thickness;
final Radius? radius;
final bool Function(ScrollNotification)? notificationPredicate;
final bool? interactive;
final ScrollbarOrientation? scrollbarOrientation;
const InterScrollbar({ const InterScrollbar({
super.key, super.key,
required this.child, required this.child,
this.controller, required this.controller,
this.thumbVisibility,
this.trackVisibility,
this.thickness,
this.radius,
this.notificationPredicate,
this.interactive,
this.scrollbarOrientation,
}); });
@override @override
@ -32,38 +19,9 @@ class InterScrollbar extends HookWidget {
if (DesktopTools.platform.isDesktop) return child; if (DesktopTools.platform.isDesktop) return child;
return ScrollbarTheme( return DraggableScrollbar.semicircle(
data: theme.scrollbarTheme.copyWith(
crossAxisMargin: 10,
minThumbLength: 80,
thickness: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged) ||
states.contains(MaterialState.pressed)) {
return 40;
}
return 20;
}),
radius: const Radius.circular(20),
thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged)) {
return theme.colorScheme.onSurface.withOpacity(0.5);
}
return theme.colorScheme.onSurface.withOpacity(0.3);
}),
),
child: Scrollbar(
controller: controller, controller: controller,
thumbVisibility: thumbVisibility,
trackVisibility: trackVisibility,
thickness: thickness,
radius: radius,
notificationPredicate: notificationPredicate,
interactive: interactive ?? true,
scrollbarOrientation: scrollbarOrientation,
child: child, child: child,
),
); );
} }
} }

View File

@ -79,8 +79,6 @@ class GenrePage extends HookConsumerWidget {
const ShimmerCategories() const ShimmerCategories()
else else
Expanded( Expanded(
child: InterScrollbar(
controller: scrollController,
child: ListView.builder( child: ListView.builder(
controller: scrollController, controller: scrollController,
itemCount: categories.length, itemCount: categories.length,
@ -102,7 +100,6 @@ class GenrePage extends HookConsumerWidget {
}, },
), ),
), ),
),
], ],
), ),
), ),

View File

@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
@ -47,9 +46,7 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages], [newReleases.pages],
); );
return InterScrollbar( return ListView(
controller: controller,
child: ListView(
controller: controller, controller: controller,
children: [ children: [
if (!featuredPlaylistsQuery.hasPageData && if (!featuredPlaylistsQuery.hasPageData &&
@ -64,7 +61,8 @@ class PersonalizedPage extends HookConsumerWidget {
), ),
if (auth != null && if (auth != null &&
newReleases.hasPageData && newReleases.hasPageData &&
userArtistsQuery.hasData) userArtistsQuery.hasData &&
!newReleases.isLoadingNextPage)
HorizontalPlaybuttonCardView<Album>( HorizontalPlaybuttonCardView<Album>(
items: albums, items: albums,
title: Text(context.l10n.new_releases), title: Text(context.l10n.new_releases),
@ -87,7 +85,6 @@ class PersonalizedPage extends HookConsumerWidget {
); );
}) })
], ],
),
); );
} }
} }

View File

@ -71,8 +71,13 @@ class SearchPage extends HookConsumerWidget {
searchTerm.isNotEmpty; searchTerm.isNotEmpty;
final resultWidget = HookBuilder( final resultWidget = HookBuilder(
builder: (context) => InterScrollbar( builder: (context) {
final controller = useScrollController();
return InterScrollbar(
controller: controller,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: controller,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea( child: SafeArea(
@ -90,7 +95,8 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
), );
},
); );
return SafeArea( return SafeArea(

View File

@ -15,6 +15,7 @@ class BlackListPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final controller = useScrollController();
final blacklist = ref.watch(BlackListNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider);
final searchText = useState(""); final searchText = useState("");
@ -58,7 +59,9 @@ class BlackListPage extends HookConsumerWidget {
), ),
), ),
InterScrollbar( InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
shrinkWrap: true, shrinkWrap: true,
itemCount: filteredBlacklist.length, itemCount: filteredBlacklist.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -52,6 +52,7 @@ class LogsPage extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = useScrollController();
final logs = useState<List<({DateTime? date, String body})>>([]); final logs = useState<List<({DateTime? date, String body})>>([]);
final rawLogs = useRef<String>(""); final rawLogs = useRef<String>("");
final path = useRef<File?>(null); final path = useRef<File?>(null);
@ -93,7 +94,9 @@ class LogsPage extends HookWidget {
), ),
body: SafeArea( body: SafeArea(
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: logs.value.length, itemCount: logs.value.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final log = logs.value[index]; final log = logs.value[index];

View File

@ -16,6 +16,7 @@ class SettingsAboutSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.about, heading: context.l10n.about,
@ -68,7 +69,7 @@ class SettingsAboutSection extends HookConsumerWidget {
secondary: const Icon(SpotubeIcons.update), secondary: const Icon(SpotubeIcons.update),
title: Text(context.l10n.check_for_updates), title: Text(context.l10n.check_for_updates),
value: preferences.checkUpdate, value: preferences.checkUpdate,
onChanged: (checked) => preferences.setCheckUpdate(checked), onChanged: (checked) => preferencesNotifier.setCheckUpdate(checked),
), ),
ListTile( ListTile(
leading: const Icon(SpotubeIcons.info), leading: const Icon(SpotubeIcons.info),

View File

@ -15,6 +15,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final pickColorScheme = useCallback(() { final pickColorScheme = useCallback(() {
return () => showDialog( return () => showDialog(
context: context, context: context,
@ -33,7 +34,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
value: preferences.layoutMode, value: preferences.layoutMode,
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setLayoutMode(value); preferencesNotifier.setLayoutMode(value);
} }
}, },
options: [ options: [
@ -71,7 +72,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setThemeMode(value); preferencesNotifier.setThemeMode(value);
} }
}, },
), ),
@ -80,7 +81,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
title: Text(context.l10n.use_amoled_mode), title: Text(context.l10n.use_amoled_mode),
subtitle: Text(context.l10n.pitch_dark_theme), subtitle: Text(context.l10n.pitch_dark_theme),
value: preferences.amoledDarkTheme, value: preferences.amoledDarkTheme,
onChanged: preferences.setAmoledDarkTheme, onChanged: preferencesNotifier.setAmoledDarkTheme,
), ),
ListTile( ListTile(
leading: const Icon(SpotubeIcons.palette), leading: const Icon(SpotubeIcons.palette),
@ -101,7 +102,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
title: Text(context.l10n.sync_album_color), title: Text(context.l10n.sync_album_color),
subtitle: Text(context.l10n.sync_album_color_description), subtitle: Text(context.l10n.sync_album_color_description),
value: preferences.albumColorSync, value: preferences.albumColorSync,
onChanged: preferences.setAlbumColorSync, onChanged: preferencesNotifier.setAlbumColorSync,
), ),
], ],
); );

View File

@ -12,6 +12,7 @@ class SettingsDesktopSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.desktop, heading: context.l10n.desktop,
@ -32,7 +33,7 @@ class SettingsDesktopSection extends HookConsumerWidget {
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setCloseBehavior(value); preferencesNotifier.setCloseBehavior(value);
} }
}, },
), ),
@ -40,13 +41,13 @@ class SettingsDesktopSection extends HookConsumerWidget {
secondary: const Icon(SpotubeIcons.tray), secondary: const Icon(SpotubeIcons.tray),
title: Text(context.l10n.show_tray_icon), title: Text(context.l10n.show_tray_icon),
value: preferences.showSystemTrayIcon, value: preferences.showSystemTrayIcon,
onChanged: preferences.setShowSystemTrayIcon, onChanged: preferencesNotifier.setShowSystemTrayIcon,
), ),
SwitchListTile( SwitchListTile(
secondary: const Icon(SpotubeIcons.window), secondary: const Icon(SpotubeIcons.window),
title: Text(context.l10n.use_system_title_bar), title: Text(context.l10n.use_system_title_bar),
value: preferences.systemTitleBar, value: preferences.systemTitleBar,
onChanged: preferences.setSystemTitleBar, onChanged: preferencesNotifier.setSystemTitleBar,
), ),
], ],
); );

View File

@ -14,6 +14,7 @@ class SettingsDownloadsSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final pickDownloadLocation = useCallback(() async { final pickDownloadLocation = useCallback(() async {
@ -22,13 +23,13 @@ class SettingsDownloadsSection extends HookConsumerWidget {
initialDirectory: preferences.downloadLocation, initialDirectory: preferences.downloadLocation,
); );
if (dirStr == null) return; if (dirStr == null) return;
preferences.setDownloadLocation(dirStr); preferencesNotifier.setDownloadLocation(dirStr);
} else { } else {
String? dirStr = await getDirectoryPath( String? dirStr = await getDirectoryPath(
initialDirectory: preferences.downloadLocation, initialDirectory: preferences.downloadLocation,
); );
if (dirStr == null) return; if (dirStr == null) return;
preferences.setDownloadLocation(dirStr); preferencesNotifier.setDownloadLocation(dirStr);
} }
}, [preferences.downloadLocation]); }, [preferences.downloadLocation]);

View File

@ -17,6 +17,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
return SectionCardWithHeading( return SectionCardWithHeading(
@ -26,7 +27,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
value: preferences.locale, value: preferences.locale,
onChanged: (locale) { onChanged: (locale) {
if (locale == null) return; if (locale == null) return;
preferences.setLocale(locale); preferencesNotifier.setLocale(locale);
}, },
title: Text(context.l10n.language), title: Text(context.l10n.language),
secondary: const Icon(SpotubeIcons.language), secondary: const Icon(SpotubeIcons.language),
@ -57,7 +58,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget {
value: preferences.recommendationMarket, value: preferences.recommendationMarket,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences.setRecommendationMarket(value); preferencesNotifier.setRecommendationMarket(value);
}, },
options: spotifyMarkets options: spotifyMarkets
.map( .map(

View File

@ -18,6 +18,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final theme = Theme.of(context); final theme = Theme.of(context);
return SectionCardWithHeading( return SectionCardWithHeading(
@ -43,7 +44,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setAudioQuality(value); preferencesNotifier.setAudioQuality(value);
} }
}, },
), ),
@ -59,7 +60,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences.setAudioSource(value); preferencesNotifier.setAudioSource(value);
}, },
), ),
AnimatedSwitcher( AnimatedSwitcher(
@ -117,7 +118,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setPipedInstance(value); preferencesNotifier.setPipedInstance(value);
} }
}, },
); );
@ -145,7 +146,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences.setSearchMode(value); preferencesNotifier.setSearchMode(value);
}, },
), ),
), ),
@ -159,7 +160,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
title: Text(context.l10n.skip_non_music), title: Text(context.l10n.skip_non_music),
value: preferences.skipNonMusic, value: preferences.skipNonMusic,
onChanged: (state) { onChanged: (state) {
preferences.setSkipNonMusic(state); preferencesNotifier.setSkipNonMusic(state);
}, },
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
@ -177,7 +178,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
secondary: const Icon(SpotubeIcons.normalize), secondary: const Icon(SpotubeIcons.normalize),
title: Text(context.l10n.normalize_audio), title: Text(context.l10n.normalize_audio),
value: preferences.normalizeAudio, value: preferences.normalizeAudio,
onChanged: preferences.setNormalizeAudio, onChanged: preferencesNotifier.setNormalizeAudio,
), ),
if (preferences.audioSource != AudioSource.jiosaavn) if (preferences.audioSource != AudioSource.jiosaavn)
AdaptiveSelectTile<SourceCodecs>( AdaptiveSelectTile<SourceCodecs>(
@ -196,7 +197,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences.setStreamMusicCodec(value); preferencesNotifier.setStreamMusicCodec(value);
}, },
), ),
if (preferences.audioSource != AudioSource.jiosaavn) if (preferences.audioSource != AudioSource.jiosaavn)
@ -216,7 +217,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences.setDownloadMusicCodec(value); preferencesNotifier.setDownloadMusicCodec(value);
}, },
), ),
], ],

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
@ -20,7 +21,8 @@ class SettingsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final controller = useScrollController();
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
return SafeArea( return SafeArea(
bottom: false, bottom: false,
@ -36,7 +38,9 @@ class SettingsPage extends HookConsumerWidget {
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView( child: ListView(
controller: controller,
children: [ children: [
const SettingsAccountSection(), const SettingsAccountSection(),
const SettingsLanguageRegionSection(), const SettingsLanguageRegionSection(),
@ -49,7 +53,7 @@ class SettingsPage extends HookConsumerWidget {
const SettingsAboutSection(), const SettingsAboutSection(),
Center( Center(
child: FilledButton( child: FilledButton(
onPressed: preferences.reset, onPressed: preferencesNotifier.reset,
child: Text(context.l10n.restore_defaults), child: Text(context.l10n.restore_defaults),
), ),
), ),

View File

@ -217,6 +217,26 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
Catcher2.reportCheckedError(e, stackTrace); Catcher2.reportCheckedError(e, stackTrace);
} }
}); });
String? lastScrobbled;
audioPlayer.positionStream.listen((position) {
try {
final uid = state.activeTrack is LocalTrack
? (state.activeTrack as LocalTrack).path
: state.activeTrack?.id;
if (state.activeTrack == null ||
lastScrobbled == uid ||
position.inSeconds < 30) {
return;
}
scrobbler.scrobble(state.activeTrack!);
lastScrobbled = uid;
} catch (e, stack) {
Catcher2.reportCheckedError(e, stack);
}
});
}(); }();
} }
@ -631,30 +651,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
@override @override
set state(state) { set state(state) {
final hasActiveTrackChanged = super.state.activeTrack is SourcedTrack
? state.activeTrack?.id != super.state.activeTrack?.id
: super.state.activeTrack is LocalTrack &&
state.activeTrack is LocalTrack
? (super.state.activeTrack as LocalTrack).path !=
(state.activeTrack as LocalTrack).path
: super.state.activeTrack?.id != state.activeTrack?.id;
final oldTrack = super.state.activeTrack;
super.state = state; super.state = state;
if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { if (state.tracks.isEmpty && ref.read(paletteProvider) != null) {
ref.read(paletteProvider.notifier).state = null; ref.read(paletteProvider.notifier).state = null;
} else { } else {
updatePalette(); updatePalette();
} }
audioPlayer.position.then((position) {
final isMoreThan30secs = position != null &&
(position == Duration.zero || position.inSeconds > 30);
if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) {
scrobbler.scrobble(oldTrack);
}
});
} }
@override @override

View File

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -14,7 +13,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/persisted_change_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -64,220 +63,103 @@ enum SearchMode {
} }
} }
class UserPreferences extends PersistedChangeNotifier { class UserPreferences {
SourceQualities audioQuality; final SourceQualities audioQuality;
bool albumColorSync; final bool albumColorSync;
bool amoledDarkTheme; final bool amoledDarkTheme;
bool checkUpdate; final bool checkUpdate;
bool normalizeAudio; final bool normalizeAudio;
bool showSystemTrayIcon; final bool showSystemTrayIcon;
bool skipNonMusic; final bool skipNonMusic;
bool systemTitleBar; final bool systemTitleBar;
CloseBehavior closeBehavior; final CloseBehavior closeBehavior;
late SpotubeColor accentColorScheme; final SpotubeColor accentColorScheme;
LayoutMode layoutMode; final LayoutMode layoutMode;
Locale locale; final Locale locale;
Market recommendationMarket; final Market recommendationMarket;
SearchMode searchMode; final SearchMode searchMode;
String downloadLocation; String downloadLocation;
String pipedInstance; final String pipedInstance;
ThemeMode themeMode; final ThemeMode themeMode;
AudioSource audioSource; final AudioSource audioSource;
SourceCodecs streamMusicCodec; final SourceCodecs streamMusicCodec;
SourceCodecs downloadMusicCodec; final SourceCodecs downloadMusicCodec;
final Ref ref; UserPreferences({
required SourceQualities? audioQuality,
UserPreferences( required bool? albumColorSync,
this.ref, { required bool? amoledDarkTheme,
this.recommendationMarket = Market.US, required bool? checkUpdate,
this.themeMode = ThemeMode.system, required bool? normalizeAudio,
this.layoutMode = LayoutMode.adaptive, required bool? showSystemTrayIcon,
this.albumColorSync = true, required bool? skipNonMusic,
this.checkUpdate = true, required bool? systemTitleBar,
this.audioQuality = SourceQualities.high, required CloseBehavior? closeBehavior,
this.downloadLocation = "", required SpotubeColor? accentColorScheme,
this.closeBehavior = CloseBehavior.close, required LayoutMode? layoutMode,
this.showSystemTrayIcon = true, required Locale? locale,
this.locale = const Locale("system", "system"), required Market? recommendationMarket,
this.pipedInstance = "https://pipedapi.kavin.rocks", required SearchMode? searchMode,
this.searchMode = SearchMode.youtube, required String? downloadLocation,
this.skipNonMusic = true, required String? pipedInstance,
this.audioSource = AudioSource.youtube, required ThemeMode? themeMode,
this.systemTitleBar = false, required AudioSource? audioSource,
this.amoledDarkTheme = false, required SourceCodecs? streamMusicCodec,
this.normalizeAudio = true, required SourceCodecs? downloadMusicCodec,
this.streamMusicCodec = SourceCodecs.weba, }) : accentColorScheme =
this.downloadMusicCodec = SourceCodecs.m4a, accentColorScheme ?? const SpotubeColor(0xFF2196F3, name: "Blue"),
SpotubeColor? accentColorScheme, albumColorSync = albumColorSync ?? true,
}) : super() { amoledDarkTheme = amoledDarkTheme ?? false,
this.accentColorScheme = audioQuality = audioQuality ?? SourceQualities.high,
accentColorScheme ?? SpotubeColor(Colors.blue.value, name: "Blue"); checkUpdate = checkUpdate ?? true,
if (downloadLocation.isEmpty && !kIsWeb) { closeBehavior = closeBehavior ?? CloseBehavior.close,
downloadLocation = downloadLocation ?? "",
downloadMusicCodec = downloadMusicCodec ?? SourceCodecs.m4a,
layoutMode = layoutMode ?? LayoutMode.adaptive,
locale = locale ?? const Locale("system", "system"),
normalizeAudio = normalizeAudio ?? true,
pipedInstance = pipedInstance ?? "https://pipedapi.kavin.rocks",
recommendationMarket = recommendationMarket ?? Market.US,
searchMode = searchMode ?? SearchMode.youtube,
showSystemTrayIcon = showSystemTrayIcon ?? true,
skipNonMusic = skipNonMusic ?? true,
streamMusicCodec = streamMusicCodec ?? SourceCodecs.weba,
systemTitleBar = systemTitleBar ?? false,
themeMode = themeMode ?? ThemeMode.system,
audioSource = audioSource ?? AudioSource.youtube {
if (downloadLocation == null) {
_getDefaultDownloadDirectory().then( _getDefaultDownloadDirectory().then(
(value) { (value) => this.downloadLocation = value,
downloadLocation = value;
},
); );
} }
} }
void reset() { factory UserPreferences.withDefaults() {
setRecommendationMarket(Market.US); return UserPreferences(
setThemeMode(ThemeMode.system); audioQuality: null,
setLayoutMode(LayoutMode.adaptive); albumColorSync: null,
setAlbumColorSync(true); amoledDarkTheme: null,
setCheckUpdate(true); checkUpdate: null,
setAudioQuality(SourceQualities.high); normalizeAudio: null,
setDownloadLocation(""); showSystemTrayIcon: null,
setCloseBehavior(CloseBehavior.close); skipNonMusic: null,
setShowSystemTrayIcon(true); systemTitleBar: null,
setLocale(const Locale("system", "system")); closeBehavior: null,
setPipedInstance("https://pipedapi.kavin.rocks"); accentColorScheme: null,
setSearchMode(SearchMode.youtube); layoutMode: null,
setSkipNonMusic(true); locale: null,
setAudioSource(AudioSource.youtube); recommendationMarket: null,
setSystemTitleBar(false); searchMode: null,
setAmoledDarkTheme(false); downloadLocation: null,
setNormalizeAudio(true); pipedInstance: null,
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue")); themeMode: null,
setStreamMusicCodec(SourceCodecs.weba); audioSource: null,
setDownloadMusicCodec(SourceCodecs.m4a); streamMusicCodec: null,
} downloadMusicCodec: null,
void setStreamMusicCodec(SourceCodecs codec) {
streamMusicCodec = codec;
notifyListeners();
updatePersistence();
}
void setDownloadMusicCodec(SourceCodecs codec) {
downloadMusicCodec = codec;
notifyListeners();
updatePersistence();
}
void setThemeMode(ThemeMode mode) {
themeMode = mode;
notifyListeners();
updatePersistence();
}
void setRecommendationMarket(Market country) {
recommendationMarket = country;
notifyListeners();
updatePersistence();
}
void setAccentColorScheme(SpotubeColor color) {
accentColorScheme = color;
notifyListeners();
updatePersistence();
}
void setAlbumColorSync(bool sync) {
albumColorSync = sync;
if (!sync) {
ref.read(paletteProvider.notifier).state = null;
} else {
ref.read(ProxyPlaylistNotifier.notifier).updatePalette();
}
notifyListeners();
updatePersistence();
}
void setCheckUpdate(bool check) {
checkUpdate = check;
notifyListeners();
updatePersistence();
}
void setAudioQuality(SourceQualities quality) {
audioQuality = quality;
notifyListeners();
updatePersistence();
}
void setDownloadLocation(String downloadDir) {
if (downloadDir.isEmpty) return;
downloadLocation = downloadDir;
notifyListeners();
updatePersistence();
}
void setLayoutMode(LayoutMode mode) {
layoutMode = mode;
notifyListeners();
updatePersistence();
}
void setCloseBehavior(CloseBehavior behavior) {
closeBehavior = behavior;
notifyListeners();
updatePersistence();
}
void setShowSystemTrayIcon(bool show) {
showSystemTrayIcon = show;
notifyListeners();
updatePersistence();
}
void setLocale(Locale locale) {
this.locale = locale;
notifyListeners();
updatePersistence();
}
void setPipedInstance(String instance) {
pipedInstance = instance;
notifyListeners();
updatePersistence();
}
void setSearchMode(SearchMode mode) {
searchMode = mode;
notifyListeners();
updatePersistence();
}
void setSkipNonMusic(bool skip) {
skipNonMusic = skip;
notifyListeners();
updatePersistence();
}
void setAudioSource(AudioSource type) {
audioSource = type;
notifyListeners();
updatePersistence();
}
void setSystemTitleBar(bool isSystemTitleBar) {
systemTitleBar = isSystemTitleBar;
if (DesktopTools.platform.isDesktop) {
DesktopTools.window.setTitleBarStyle(
systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
); );
} }
notifyListeners();
updatePersistence();
}
void setAmoledDarkTheme(bool isAmoled) { static Future<String> _getDefaultDownloadDirectory() async {
amoledDarkTheme = isAmoled;
notifyListeners();
updatePersistence();
}
void setNormalizeAudio(bool normalize) {
normalizeAudio = normalize;
audioPlayer.setAudioNormalization(normalize);
notifyListeners();
updatePersistence();
}
Future<String> _getDefaultDownloadDirectory() async {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
if (kIsMacOS) { if (kIsMacOS) {
@ -289,102 +171,71 @@ class UserPreferences extends PersistedChangeNotifier {
}); });
} }
@override static Future<UserPreferences> fromJson(Map<String, dynamic> json) async {
FutureOr<void> loadFromLocal(Map<String, dynamic> map) async { final systemTitleBar = json["systemTitleBar"] ?? false;
recommendationMarket = Market.values.firstWhere( if (DesktopTools.platform.isDesktop) {
(market) => await DesktopTools.window.setTitleBarStyle(
market.name == (map["recommendationMarket"] ?? recommendationMarket), systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
orElse: () => Market.US,
); );
checkUpdate = map["checkUpdate"] ?? checkUpdate;
themeMode = ThemeMode.values[map["themeMode"] ?? 0];
accentColorScheme = map["accentColorScheme"] != null
? SpotubeColor.fromString(map["accentColorScheme"])
: accentColorScheme;
albumColorSync = map["albumColorSync"] ?? albumColorSync;
audioQuality = map["audioQuality"] != null
? SourceQualities.values[map["audioQuality"]]
: audioQuality;
if (!kIsWeb) {
downloadLocation =
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
} }
layoutMode = LayoutMode.values.firstWhere( final normalizeAudio = json["normalizeAudio"] ?? true;
(mode) => mode.name == map["layoutMode"],
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
);
closeBehavior = map["closeBehavior"] != null
? CloseBehavior.values[map["closeBehavior"]]
: closeBehavior;
showSystemTrayIcon = map["showSystemTrayIcon"] ?? showSystemTrayIcon;
final localeMap = map["locale"] != null ? jsonDecode(map["locale"]) : null;
locale =
localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale;
pipedInstance = map["pipedInstance"] ?? pipedInstance;
searchMode = SearchMode.values.firstWhere(
(mode) => mode.name == map["searchMode"],
orElse: () => SearchMode.youtube,
);
skipNonMusic = map["skipNonMusic"] ?? skipNonMusic;
audioSource = AudioSource.values.firstWhere(
(type) => type.name == map["youtubeApiType"],
orElse: () => AudioSource.youtube,
);
systemTitleBar = map["systemTitleBar"] ?? systemTitleBar;
// updates the title bar
setSystemTitleBar(systemTitleBar);
amoledDarkTheme = map["amoledDarkTheme"] ?? amoledDarkTheme;
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
audioPlayer.setAudioNormalization(normalizeAudio); audioPlayer.setAudioNormalization(normalizeAudio);
streamMusicCodec = SourceCodecs.values.firstWhere( final Map<String, dynamic>? localeMap =
(codec) => codec.name == map["streamMusicCodec"], json["locale"] == null ? null : jsonDecode(json["locale"]);
orElse: () => SourceCodecs.weba,
);
downloadMusicCodec = SourceCodecs.values.firstWhere( return UserPreferences(
(codec) => codec.name == map["downloadMusicCodec"], accentColorScheme: json["accentColorScheme"] == null
orElse: () => SourceCodecs.m4a, ? null
: SpotubeColor.fromString(json["accentColorScheme"]),
albumColorSync: json["albumColorSync"],
amoledDarkTheme: json["amoledDarkTheme"],
audioQuality: SourceQualities.values[json["audioQuality"]],
checkUpdate: json["checkUpdate"],
closeBehavior: CloseBehavior.values[json["closeBehavior"]],
downloadLocation:
json["downloadLocation"] ?? await _getDefaultDownloadDirectory(),
downloadMusicCodec: SourceCodecs.values[json["downloadMusicCodec"]],
layoutMode: LayoutMode.values[json["layoutMode"]],
locale:
localeMap == null ? null : Locale(localeMap["lc"], localeMap["cc"]),
normalizeAudio: json["normalizeAudio"],
pipedInstance: json["pipedInstance"],
recommendationMarket: Market.values[json["recommendationMarket"]],
searchMode: SearchMode.values[json["searchMode"]],
showSystemTrayIcon: json["showSystemTrayIcon"],
skipNonMusic: json["skipNonMusic"],
streamMusicCodec: SourceCodecs.values[json["streamMusicCodec"]],
systemTitleBar: json["systemTitleBar"],
themeMode: ThemeMode.values[json["themeMode"]],
audioSource: AudioSource.values[json["audioSource"]],
); );
} }
@override Map<String, dynamic> toJson() {
FutureOr<Map<String, dynamic>> toMap() {
return { return {
"recommendationMarket": recommendationMarket.name, "recommendationMarket": recommendationMarket.index,
"themeMode": themeMode.index, "themeMode": themeMode.index,
"accentColorScheme": accentColorScheme.toString(), "accentColorScheme": accentColorScheme.toString(),
"albumColorSync": albumColorSync, "albumColorSync": albumColorSync,
"checkUpdate": checkUpdate, "checkUpdate": checkUpdate,
"audioQuality": audioQuality.index, "audioQuality": audioQuality.index,
"downloadLocation": downloadLocation, "downloadLocation": downloadLocation,
"layoutMode": layoutMode.name, "layoutMode": layoutMode.index,
"closeBehavior": closeBehavior.index, "closeBehavior": closeBehavior.index,
"showSystemTrayIcon": showSystemTrayIcon, "showSystemTrayIcon": showSystemTrayIcon,
"locale": "locale":
jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}), jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}),
"pipedInstance": pipedInstance, "pipedInstance": pipedInstance,
"searchMode": searchMode.name, "searchMode": searchMode.index,
"skipNonMusic": skipNonMusic, "skipNonMusic": skipNonMusic,
"youtubeApiType": audioSource.name, "audioSource": audioSource.index,
'systemTitleBar': systemTitleBar, 'systemTitleBar': systemTitleBar,
"amoledDarkTheme": amoledDarkTheme, "amoledDarkTheme": amoledDarkTheme,
"normalizeAudio": normalizeAudio, "normalizeAudio": normalizeAudio,
"streamMusicCodec": streamMusicCodec.name, "streamMusicCodec": streamMusicCodec.index,
"downloadMusicCodec": downloadMusicCodec.name, "downloadMusicCodec": downloadMusicCodec.index,
}; };
} }
@ -402,12 +253,16 @@ class UserPreferences extends PersistedChangeNotifier {
String? pipedInstance, String? pipedInstance,
SearchMode? searchMode, SearchMode? searchMode,
bool? skipNonMusic, bool? skipNonMusic,
AudioSource? youtubeApiType, AudioSource? audioSource,
Market? recommendationMarket, Market? recommendationMarket,
bool? saveTrackLyrics, bool? saveTrackLyrics,
bool? amoledDarkTheme,
bool? normalizeAudio,
SourceCodecs? downloadMusicCodec,
SourceCodecs? streamMusicCodec,
bool? systemTitleBar,
}) { }) {
return UserPreferences( return UserPreferences(
ref,
themeMode: themeMode ?? this.themeMode, themeMode: themeMode ?? this.themeMode,
accentColorScheme: accentColorScheme ?? this.accentColorScheme, accentColorScheme: accentColorScheme ?? this.accentColorScheme,
albumColorSync: albumColorSync ?? this.albumColorSync, albumColorSync: albumColorSync ?? this.albumColorSync,
@ -421,12 +276,132 @@ class UserPreferences extends PersistedChangeNotifier {
pipedInstance: pipedInstance ?? this.pipedInstance, pipedInstance: pipedInstance ?? this.pipedInstance,
searchMode: searchMode ?? this.searchMode, searchMode: searchMode ?? this.searchMode,
skipNonMusic: skipNonMusic ?? this.skipNonMusic, skipNonMusic: skipNonMusic ?? this.skipNonMusic,
audioSource: youtubeApiType ?? this.audioSource, audioSource: audioSource ?? this.audioSource,
recommendationMarket: recommendationMarket ?? this.recommendationMarket, recommendationMarket: recommendationMarket ?? this.recommendationMarket,
amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme,
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
normalizeAudio: normalizeAudio ?? this.normalizeAudio,
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
systemTitleBar: systemTitleBar ?? this.systemTitleBar,
); );
} }
} }
final userPreferencesProvider = ChangeNotifierProvider( class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
(ref) => UserPreferences(ref), final Ref ref;
UserPreferencesNotifier(this.ref)
: super(UserPreferences.withDefaults(), "preferences");
void reset() {
state = UserPreferences.withDefaults();
}
void setStreamMusicCodec(SourceCodecs codec) {
state = state.copyWith(streamMusicCodec: codec);
}
void setDownloadMusicCodec(SourceCodecs codec) {
state = state.copyWith(downloadMusicCodec: codec);
}
void setThemeMode(ThemeMode mode) {
state = state.copyWith(themeMode: mode);
}
void setRecommendationMarket(Market country) {
state = state.copyWith(recommendationMarket: country);
}
void setAccentColorScheme(SpotubeColor color) {
state = state.copyWith(accentColorScheme: color);
}
void setAlbumColorSync(bool sync) {
state = state.copyWith(albumColorSync: sync);
if (!sync) {
ref.read(paletteProvider.notifier).state = null;
} else {
ref.read(ProxyPlaylistNotifier.notifier).updatePalette();
}
}
void setCheckUpdate(bool check) {
state = state.copyWith(checkUpdate: check);
}
void setAudioQuality(SourceQualities quality) {
state = state.copyWith(audioQuality: quality);
}
void setDownloadLocation(String downloadDir) {
if (downloadDir.isEmpty) return;
state = state.copyWith(downloadLocation: downloadDir);
}
void setLayoutMode(LayoutMode mode) {
state = state.copyWith(layoutMode: mode);
}
void setCloseBehavior(CloseBehavior behavior) {
state = state.copyWith(closeBehavior: behavior);
}
void setShowSystemTrayIcon(bool show) {
state = state.copyWith(showSystemTrayIcon: show);
}
void setLocale(Locale locale) {
state = state.copyWith(locale: locale);
}
void setPipedInstance(String instance) {
state = state.copyWith(pipedInstance: instance);
}
void setSearchMode(SearchMode mode) {
state = state.copyWith(searchMode: mode);
}
void setSkipNonMusic(bool skip) {
state = state.copyWith(skipNonMusic: skip);
}
void setAudioSource(AudioSource type) {
state = state.copyWith(audioSource: type);
}
void setSystemTitleBar(bool isSystemTitleBar) {
state = state.copyWith(systemTitleBar: isSystemTitleBar);
if (DesktopTools.platform.isDesktop) {
DesktopTools.window.setTitleBarStyle(
isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
);
}
}
void setAmoledDarkTheme(bool isAmoled) {
state = state.copyWith(amoledDarkTheme: isAmoled);
}
void setNormalizeAudio(bool normalize) {
state = state.copyWith(normalizeAudio: normalize);
audioPlayer.setAudioNormalization(normalize);
}
@override
FutureOr<UserPreferences> fromJson(Map<String, dynamic> json) {
return UserPreferences.fromJson(json);
}
@override
Map<String, dynamic> toJson() {
return state.toJson();
}
}
final userPreferencesProvider =
StateNotifierProvider<UserPreferencesNotifier, UserPreferences>(
(ref) => UserPreferencesNotifier(ref),
); );

View File

@ -473,6 +473,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
draggable_scrollbar:
dependency: "direct main"
description:
path: "."
ref: cfd570035bf393de541d32e9b28808b5d7e602df
resolved-ref: cfd570035bf393de541d32e9b28808b5d7e602df
url: "https://github.com/thielepaul/flutter-draggable-scrollbar.git"
source: git
version: "0.1.0"
duration: duration:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -109,6 +109,10 @@ dependencies:
jiosaavn: jiosaavn:
git: git:
url: https://github.com/KRTirtho/jiosaavn.git url: https://github.com/KRTirtho/jiosaavn.git
draggable_scrollbar:
git:
url: https://github.com/thielepaul/flutter-draggable-scrollbar.git
ref: cfd570035bf393de541d32e9b28808b5d7e602df
dev_dependencies: dev_dependencies:
build_runner: ^2.3.2 build_runner: ^2.3.2