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

View File

@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.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/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
@ -81,68 +82,71 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator(
onRefresh: playlistsQuery.refresh,
child: SafeArea(
child: CustomScrollView(
child: InterScrollbar(
controller: controller,
slivers: [
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SearchBar(
onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter),
),
),
Row(
children: [
const SizedBox(width: 10),
const PlaylistCreateDialogButton(),
const SizedBox(width: 10),
ElevatedButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist),
onPressed: () {
GoRouter.of(context).push("/library/generate");
},
child: CustomScrollView(
controller: controller,
slivers: [
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SearchBar(
onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter),
),
const SizedBox(width: 10),
],
),
],
),
Row(
children: [
const SizedBox(width: 10),
const PlaylistCreateDialogButton(),
const SizedBox(width: 10),
ElevatedButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist),
onPressed: () {
GoRouter.of(context).push("/library/generate");
},
),
const SizedBox(width: 10),
],
),
],
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 10),
),
SliverGrid.builder(
itemCount: playlists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
const SliverToBoxAdapter(
child: SizedBox(height: 10),
),
itemBuilder: (context, index) {
if (index == playlists.length) {
if (!playlistsQuery.hasNextPage) {
return const SizedBox.shrink();
SliverGrid.builder(
itemCount: playlists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
if (index == playlists.length) {
if (!playlistsQuery.hasNextPage) {
return const SizedBox.shrink();
}
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: playlistsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
);
}
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: playlistsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
);
}
return PlaylistCard(playlists[index]);
},
)
],
return PlaylistCard(playlists[index]);
},
)
],
),
),
),
);

View File

@ -44,6 +44,7 @@ class PlayerQueue extends HookConsumerWidget {
topRight: Radius.circular(10),
);
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized(
@ -108,171 +109,166 @@ class PlayerQueue extends HookConsumerWidget {
searchText.value = '';
}
},
child: LayoutBuilder(builder: (context, constraints) {
return Column(
children: [
if (!floating)
Container(
height: 5,
width: 100,
margin: const EdgeInsets.only(bottom: 5, top: 2),
decoration: BoxDecoration(
color: headlineColor,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
if (!floating)
Container(
height: 5,
width: 100,
margin: const EdgeInsets.only(bottom: 5, top: 2),
decoration: BoxDecoration(
color: headlineColor,
borderRadius: BorderRadius.circular(20),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (constraints.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
Text(
context.l10n.tracks_in_queue(tracks.length),
style: TextStyle(
color: headlineColor,
fontWeight: FontWeight.bold,
fontSize: 18,
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
Text(
context.l10n.tracks_in_queue(tracks.length),
style: TextStyle(
color: headlineColor,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const Spacer(),
],
if (mediaQuery.mdAndUp || isSearching.value)
TextField(
onChanged: (value) {
searchText.value = value;
},
decoration: InputDecoration(
hintText: context.l10n.search,
isDense: true,
prefixIcon: mediaQuery.smAndDown
? IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
),
onPressed: () {
isSearching.value = false;
searchText.value = '';
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size.square(20),
),
)
: const Icon(SpotubeIcons.filter),
constraints: BoxConstraints(
maxHeight: 40,
maxWidth: mediaQuery.smAndDown
? mediaQuery.size.width - 40
: 300,
),
),
const Spacer(),
],
if (constraints.mdAndUp || isSearching.value)
TextField(
onChanged: (value) {
searchText.value = value;
},
decoration: InputDecoration(
hintText: context.l10n.search,
isDense: true,
prefixIcon: constraints.smAndDown
? IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
),
onPressed: () {
isSearching.value = false;
searchText.value = '';
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size.square(20),
),
)
: const Icon(SpotubeIcons.filter),
constraints: BoxConstraints(
maxHeight: 40,
maxWidth: constraints.smAndDown
? constraints.maxWidth - 20
: 300,
)
else
IconButton.filledTonal(
icon: const Icon(SpotubeIcons.filter),
onPressed: () {
isSearching.value = !isSearching.value;
},
),
if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor:
theme.scaffoldBackgroundColor.withOpacity(0.5),
foregroundColor: theme.textTheme.headlineSmall?.color,
),
child: Row(
children: [
const Icon(SpotubeIcons.playlistRemove),
const SizedBox(width: 5),
Text(context.l10n.clear_all),
],
),
onPressed: () {
playlistNotifier.stop();
Navigator.of(context).pop();
},
),
const SizedBox(width: 10),
],
],
),
const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty)
Flexible(
child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex);
},
scrollController: controller,
itemCount: tracks.length,
shrinkWrap: true,
buildDefaultDragHandles: false,
itemBuilder: (context, i) {
final track = tracks.elementAt(i);
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
),
),
)
else
IconButton.filledTonal(
icon: const Icon(SpotubeIcons.filter),
onPressed: () {
isSearching.value = !isSearching.value;
},
),
if (constraints.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor:
theme.scaffoldBackgroundColor.withOpacity(0.5),
foregroundColor:
theme.textTheme.headlineSmall?.color,
),
child: Row(
children: [
const Icon(SpotubeIcons.playlistRemove),
const SizedBox(width: 5),
Text(context.l10n.clear_all),
],
),
onPressed: () {
playlistNotifier.stop();
Navigator.of(context).pop();
},
),
const SizedBox(width: 10),
],
],
),
const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty)
Flexible(
child: InterScrollbar(
);
},
),
)
else
Flexible(
child: InterScrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex);
},
scrollController: controller,
itemCount: tracks.length,
shrinkWrap: true,
buildDefaultDragHandles: false,
itemBuilder: (context, i) {
final track = tracks.elementAt(i);
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child:
const Icon(SpotubeIcons.dragHandle),
),
],
),
),
);
},
),
),
)
else
Flexible(
child: InterScrollbar(
child: ListView.builder(
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
),
);
},
),
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
),
);
},
),
),
],
);
}),
),
],
),
),
),
),

View File

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

View File

@ -49,6 +49,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final scheme = preferences.accentColorScheme;
final active = useState<String>(colorsMap.firstWhere(
(element) {
@ -57,7 +58,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
).name);
onOk() {
preferences.setAccentColorScheme(
preferencesNotifier.setAccentColorScheme(
colorsMap.firstWhere(
(element) {
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_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class InterScrollbar extends HookWidget {
final Widget child;
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;
final ScrollController controller;
const InterScrollbar({
super.key,
required this.child,
this.controller,
this.thumbVisibility,
this.trackVisibility,
this.thickness,
this.radius,
this.notificationPredicate,
this.interactive,
this.scrollbarOrientation,
required this.controller,
});
@override
@ -32,38 +19,9 @@ class InterScrollbar extends HookWidget {
if (DesktopTools.platform.isDesktop) return child;
return ScrollbarTheme(
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,
thumbVisibility: thumbVisibility,
trackVisibility: trackVisibility,
thickness: thickness,
radius: radius,
notificationPredicate: notificationPredicate,
interactive: interactive ?? true,
scrollbarOrientation: scrollbarOrientation,
child: child,
),
return DraggableScrollbar.semicircle(
controller: controller,
child: child,
);
}
}

View File

@ -79,28 +79,25 @@ class GenrePage extends HookConsumerWidget {
const ShimmerCategories()
else
Expanded(
child: InterScrollbar(
child: ListView.builder(
controller: scrollController,
child: ListView.builder(
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
duration: const Duration(milliseconds: 300),
child: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? const ShimmerCategories()
: CategoryCard(categories[index]),
);
},
),
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
duration: const Duration(milliseconds: 300),
child: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? const ShimmerCategories()
: CategoryCard(categories[index]),
);
},
),
),
],

View File

@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.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/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
@ -47,47 +46,45 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages],
);
return InterScrollbar(
return ListView(
controller: controller,
child: ListView(
controller: controller,
children: [
if (!featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage)
const ShimmerCategories()
else
HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(),
title: Text(context.l10n.featured),
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null &&
newReleases.hasPageData &&
userArtistsQuery.hasData)
HorizontalPlaybuttonCardView<Album>(
items: albums,
title: Text(context.l10n.new_releases),
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
...?madeForUser.data?["content"]?["items"]?.map((item) {
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
.toList()
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists,
title: Text(item["name"] ?? ""),
hasNextPage: false,
onFetchMore: () {},
);
})
],
),
children: [
if (!featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage)
const ShimmerCategories()
else
HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(),
title: Text(context.l10n.featured),
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null &&
newReleases.hasPageData &&
userArtistsQuery.hasData &&
!newReleases.isLoadingNextPage)
HorizontalPlaybuttonCardView<Album>(
items: albums,
title: Text(context.l10n.new_releases),
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
...?madeForUser.data?["content"]?["items"]?.map((item) {
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
.toList()
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists,
title: Text(item["name"] ?? ""),
hasNextPage: false,
onFetchMore: () {},
);
})
],
);
}
}

View File

@ -71,26 +71,32 @@ class SearchPage extends HookConsumerWidget {
searchTerm.isNotEmpty;
final resultWidget = HookBuilder(
builder: (context) => InterScrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SearchTracksSection(query: searchTrack),
SearchPlaylistsSection(query: searchPlaylist),
const SizedBox(height: 20),
SearchArtistsSection(query: searchArtist),
const SizedBox(height: 20),
SearchAlbumsSection(query: searchAlbum),
],
builder: (context) {
final controller = useScrollController();
return InterScrollbar(
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SearchTracksSection(query: searchTrack),
SearchPlaylistsSection(query: searchPlaylist),
const SizedBox(height: 20),
SearchArtistsSection(query: searchArtist),
const SizedBox(height: 20),
SearchAlbumsSection(query: searchAlbum),
],
),
),
),
),
),
),
);
},
);
return SafeArea(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,6 +217,26 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
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
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;
if (state.tracks.isEmpty && ref.read(paletteProvider) != null) {
ref.read(paletteProvider.notifier).state = null;
} else {
updatePalette();
}
audioPlayer.position.then((position) {
final isMoreThan30secs = position != null &&
(position == Duration.zero || position.inSeconds > 30);
if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) {
scrobbler.scrobble(oldTrack);
}
});
}
@override

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.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/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:path/path.dart' as path;
@ -64,220 +63,103 @@ enum SearchMode {
}
}
class UserPreferences extends PersistedChangeNotifier {
SourceQualities audioQuality;
bool albumColorSync;
bool amoledDarkTheme;
bool checkUpdate;
bool normalizeAudio;
bool showSystemTrayIcon;
bool skipNonMusic;
bool systemTitleBar;
CloseBehavior closeBehavior;
late SpotubeColor accentColorScheme;
LayoutMode layoutMode;
Locale locale;
Market recommendationMarket;
SearchMode searchMode;
class UserPreferences {
final SourceQualities audioQuality;
final bool albumColorSync;
final bool amoledDarkTheme;
final bool checkUpdate;
final bool normalizeAudio;
final bool showSystemTrayIcon;
final bool skipNonMusic;
final bool systemTitleBar;
final CloseBehavior closeBehavior;
final SpotubeColor accentColorScheme;
final LayoutMode layoutMode;
final Locale locale;
final Market recommendationMarket;
final SearchMode searchMode;
String downloadLocation;
String pipedInstance;
ThemeMode themeMode;
AudioSource audioSource;
SourceCodecs streamMusicCodec;
SourceCodecs downloadMusicCodec;
final String pipedInstance;
final ThemeMode themeMode;
final AudioSource audioSource;
final SourceCodecs streamMusicCodec;
final SourceCodecs downloadMusicCodec;
final Ref ref;
UserPreferences(
this.ref, {
this.recommendationMarket = Market.US,
this.themeMode = ThemeMode.system,
this.layoutMode = LayoutMode.adaptive,
this.albumColorSync = true,
this.checkUpdate = true,
this.audioQuality = SourceQualities.high,
this.downloadLocation = "",
this.closeBehavior = CloseBehavior.close,
this.showSystemTrayIcon = true,
this.locale = const Locale("system", "system"),
this.pipedInstance = "https://pipedapi.kavin.rocks",
this.searchMode = SearchMode.youtube,
this.skipNonMusic = true,
this.audioSource = AudioSource.youtube,
this.systemTitleBar = false,
this.amoledDarkTheme = false,
this.normalizeAudio = true,
this.streamMusicCodec = SourceCodecs.weba,
this.downloadMusicCodec = SourceCodecs.m4a,
SpotubeColor? accentColorScheme,
}) : super() {
this.accentColorScheme =
accentColorScheme ?? SpotubeColor(Colors.blue.value, name: "Blue");
if (downloadLocation.isEmpty && !kIsWeb) {
UserPreferences({
required SourceQualities? audioQuality,
required bool? albumColorSync,
required bool? amoledDarkTheme,
required bool? checkUpdate,
required bool? normalizeAudio,
required bool? showSystemTrayIcon,
required bool? skipNonMusic,
required bool? systemTitleBar,
required CloseBehavior? closeBehavior,
required SpotubeColor? accentColorScheme,
required LayoutMode? layoutMode,
required Locale? locale,
required Market? recommendationMarket,
required SearchMode? searchMode,
required String? downloadLocation,
required String? pipedInstance,
required ThemeMode? themeMode,
required AudioSource? audioSource,
required SourceCodecs? streamMusicCodec,
required SourceCodecs? downloadMusicCodec,
}) : accentColorScheme =
accentColorScheme ?? const SpotubeColor(0xFF2196F3, name: "Blue"),
albumColorSync = albumColorSync ?? true,
amoledDarkTheme = amoledDarkTheme ?? false,
audioQuality = audioQuality ?? SourceQualities.high,
checkUpdate = checkUpdate ?? true,
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(
(value) {
downloadLocation = value;
},
(value) => this.downloadLocation = value,
);
}
}
void reset() {
setRecommendationMarket(Market.US);
setThemeMode(ThemeMode.system);
setLayoutMode(LayoutMode.adaptive);
setAlbumColorSync(true);
setCheckUpdate(true);
setAudioQuality(SourceQualities.high);
setDownloadLocation("");
setCloseBehavior(CloseBehavior.close);
setShowSystemTrayIcon(true);
setLocale(const Locale("system", "system"));
setPipedInstance("https://pipedapi.kavin.rocks");
setSearchMode(SearchMode.youtube);
setSkipNonMusic(true);
setAudioSource(AudioSource.youtube);
setSystemTitleBar(false);
setAmoledDarkTheme(false);
setNormalizeAudio(true);
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue"));
setStreamMusicCodec(SourceCodecs.weba);
setDownloadMusicCodec(SourceCodecs.m4a);
factory UserPreferences.withDefaults() {
return UserPreferences(
audioQuality: null,
albumColorSync: null,
amoledDarkTheme: null,
checkUpdate: null,
normalizeAudio: null,
showSystemTrayIcon: null,
skipNonMusic: null,
systemTitleBar: null,
closeBehavior: null,
accentColorScheme: null,
layoutMode: null,
locale: null,
recommendationMarket: null,
searchMode: null,
downloadLocation: null,
pipedInstance: null,
themeMode: null,
audioSource: null,
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) {
amoledDarkTheme = isAmoled;
notifyListeners();
updatePersistence();
}
void setNormalizeAudio(bool normalize) {
normalizeAudio = normalize;
audioPlayer.setAudioNormalization(normalize);
notifyListeners();
updatePersistence();
}
Future<String> _getDefaultDownloadDirectory() async {
static Future<String> _getDefaultDownloadDirectory() async {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
if (kIsMacOS) {
@ -289,102 +171,71 @@ class UserPreferences extends PersistedChangeNotifier {
});
}
@override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) async {
recommendationMarket = Market.values.firstWhere(
(market) =>
market.name == (map["recommendationMarket"] ?? recommendationMarket),
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();
static Future<UserPreferences> fromJson(Map<String, dynamic> json) async {
final systemTitleBar = json["systemTitleBar"] ?? false;
if (DesktopTools.platform.isDesktop) {
await DesktopTools.window.setTitleBarStyle(
systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
);
}
layoutMode = LayoutMode.values.firstWhere(
(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;
final normalizeAudio = json["normalizeAudio"] ?? true;
audioPlayer.setAudioNormalization(normalizeAudio);
streamMusicCodec = SourceCodecs.values.firstWhere(
(codec) => codec.name == map["streamMusicCodec"],
orElse: () => SourceCodecs.weba,
);
final Map<String, dynamic>? localeMap =
json["locale"] == null ? null : jsonDecode(json["locale"]);
downloadMusicCodec = SourceCodecs.values.firstWhere(
(codec) => codec.name == map["downloadMusicCodec"],
orElse: () => SourceCodecs.m4a,
return UserPreferences(
accentColorScheme: json["accentColorScheme"] == null
? 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
FutureOr<Map<String, dynamic>> toMap() {
Map<String, dynamic> toJson() {
return {
"recommendationMarket": recommendationMarket.name,
"recommendationMarket": recommendationMarket.index,
"themeMode": themeMode.index,
"accentColorScheme": accentColorScheme.toString(),
"albumColorSync": albumColorSync,
"checkUpdate": checkUpdate,
"audioQuality": audioQuality.index,
"downloadLocation": downloadLocation,
"layoutMode": layoutMode.name,
"layoutMode": layoutMode.index,
"closeBehavior": closeBehavior.index,
"showSystemTrayIcon": showSystemTrayIcon,
"locale":
jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}),
"pipedInstance": pipedInstance,
"searchMode": searchMode.name,
"searchMode": searchMode.index,
"skipNonMusic": skipNonMusic,
"youtubeApiType": audioSource.name,
"audioSource": audioSource.index,
'systemTitleBar': systemTitleBar,
"amoledDarkTheme": amoledDarkTheme,
"normalizeAudio": normalizeAudio,
"streamMusicCodec": streamMusicCodec.name,
"downloadMusicCodec": downloadMusicCodec.name,
"streamMusicCodec": streamMusicCodec.index,
"downloadMusicCodec": downloadMusicCodec.index,
};
}
@ -402,12 +253,16 @@ class UserPreferences extends PersistedChangeNotifier {
String? pipedInstance,
SearchMode? searchMode,
bool? skipNonMusic,
AudioSource? youtubeApiType,
AudioSource? audioSource,
Market? recommendationMarket,
bool? saveTrackLyrics,
bool? amoledDarkTheme,
bool? normalizeAudio,
SourceCodecs? downloadMusicCodec,
SourceCodecs? streamMusicCodec,
bool? systemTitleBar,
}) {
return UserPreferences(
ref,
themeMode: themeMode ?? this.themeMode,
accentColorScheme: accentColorScheme ?? this.accentColorScheme,
albumColorSync: albumColorSync ?? this.albumColorSync,
@ -421,12 +276,132 @@ class UserPreferences extends PersistedChangeNotifier {
pipedInstance: pipedInstance ?? this.pipedInstance,
searchMode: searchMode ?? this.searchMode,
skipNonMusic: skipNonMusic ?? this.skipNonMusic,
audioSource: youtubeApiType ?? this.audioSource,
audioSource: audioSource ?? this.audioSource,
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(
(ref) => UserPreferences(ref),
class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
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"
source: hosted
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:
dependency: "direct main"
description:

View File

@ -109,6 +109,10 @@ dependencies:
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:
build_runner: ^2.3.2