mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat(android): better quick scroll/drag to scroll implementation
This commit is contained in:
parent
0a6b54da36
commit
2e2c44f0af
@ -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) {
|
||||||
|
@ -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,68 +82,71 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: playlistsQuery.refresh,
|
onRefresh: playlistsQuery.refresh,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: CustomScrollView(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
slivers: [
|
child: CustomScrollView(
|
||||||
SliverToBoxAdapter(
|
controller: controller,
|
||||||
child: Column(
|
slivers: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
SliverToBoxAdapter(
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.all(10),
|
children: [
|
||||||
child: SearchBar(
|
Padding(
|
||||||
onChanged: (value) => searchText.value = value,
|
padding: const EdgeInsets.all(10),
|
||||||
hintText: context.l10n.filter_playlists,
|
child: SearchBar(
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
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");
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
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(
|
||||||
const SliverToBoxAdapter(
|
child: SizedBox(height: 10),
|
||||||
child: SizedBox(height: 10),
|
|
||||||
),
|
|
||||||
SliverGrid.builder(
|
|
||||||
itemCount: playlists.length + 1,
|
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 200,
|
|
||||||
mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
SliverGrid.builder(
|
||||||
if (index == playlists.length) {
|
itemCount: playlists.length + 1,
|
||||||
if (!playlistsQuery.hasNextPage) {
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
return const SizedBox.shrink();
|
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(
|
return PlaylistCard(playlists[index]);
|
||||||
controller: controller,
|
},
|
||||||
isGrid: true,
|
)
|
||||||
onTouchEdge: playlistsQuery.fetchNext,
|
],
|
||||||
child: const ShimmerPlaybuttonCard(count: 1),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlaylistCard(playlists[index]);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,171 +109,166 @@ 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(
|
height: 5,
|
||||||
height: 5,
|
width: 100,
|
||||||
width: 100,
|
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
||||||
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: headlineColor,
|
||||||
color: headlineColor,
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
if (constraints.mdAndUp || !isSearching.value) ...[
|
children: [
|
||||||
const SizedBox(width: 10),
|
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
||||||
Text(
|
const SizedBox(width: 10),
|
||||||
context.l10n.tracks_in_queue(tracks.length),
|
Text(
|
||||||
style: TextStyle(
|
context.l10n.tracks_in_queue(tracks.length),
|
||||||
color: headlineColor,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
color: headlineColor,
|
||||||
fontSize: 18,
|
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(),
|
)
|
||||||
],
|
else
|
||||||
if (constraints.mdAndUp || isSearching.value)
|
IconButton.filledTonal(
|
||||||
TextField(
|
icon: const Icon(SpotubeIcons.filter),
|
||||||
onChanged: (value) {
|
onPressed: () {
|
||||||
searchText.value = value;
|
isSearching.value = !isSearching.value;
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
),
|
||||||
hintText: context.l10n.search,
|
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
||||||
isDense: true,
|
const SizedBox(width: 10),
|
||||||
prefixIcon: constraints.smAndDown
|
FilledButton(
|
||||||
? IconButton(
|
style: FilledButton.styleFrom(
|
||||||
icon: const Icon(
|
backgroundColor:
|
||||||
Icons.arrow_back_ios_new_outlined,
|
theme.scaffoldBackgroundColor.withOpacity(0.5),
|
||||||
),
|
foregroundColor: theme.textTheme.headlineSmall?.color,
|
||||||
onPressed: () {
|
),
|
||||||
isSearching.value = false;
|
child: Row(
|
||||||
searchText.value = '';
|
children: [
|
||||||
},
|
const Icon(SpotubeIcons.playlistRemove),
|
||||||
style: IconButton.styleFrom(
|
const SizedBox(width: 5),
|
||||||
padding: EdgeInsets.zero,
|
Text(context.l10n.clear_all),
|
||||||
minimumSize: const Size.square(20),
|
],
|
||||||
),
|
),
|
||||||
)
|
onPressed: () {
|
||||||
: const Icon(SpotubeIcons.filter),
|
playlistNotifier.stop();
|
||||||
constraints: BoxConstraints(
|
Navigator.of(context).pop();
|
||||||
maxHeight: 40,
|
},
|
||||||
maxWidth: constraints.smAndDown
|
),
|
||||||
? constraints.maxWidth - 20
|
const SizedBox(width: 10),
|
||||||
: 300,
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: () {
|
else
|
||||||
isSearching.value = !isSearching.value;
|
Flexible(
|
||||||
},
|
child: InterScrollbar(
|
||||||
),
|
controller: controller,
|
||||||
if (constraints.mdAndUp || !isSearching.value) ...[
|
child: ListView.builder(
|
||||||
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(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: ReorderableListView.builder(
|
itemCount: filteredTracks.length,
|
||||||
onReorder: (oldIndex, newIndex) {
|
itemBuilder: (context, i) {
|
||||||
playlistNotifier.moveTrack(oldIndex, newIndex);
|
final track = filteredTracks.elementAt(i);
|
||||||
},
|
return Padding(
|
||||||
scrollController: controller,
|
padding:
|
||||||
itemCount: tracks.length,
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
shrinkWrap: true,
|
child: TrackTile(
|
||||||
buildDefaultDragHandles: false,
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
final track = tracks.elementAt(i);
|
|
||||||
return AutoScrollTag(
|
|
||||||
key: ValueKey(i),
|
|
||||||
controller: controller,
|
|
||||||
index: i,
|
index: i,
|
||||||
child: Padding(
|
track: track,
|
||||||
padding:
|
onTap: () async {
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
if (playlist.activeTrack?.id == track.id) {
|
||||||
child: TrackTile(
|
return;
|
||||||
index: i,
|
}
|
||||||
track: track,
|
await playlistNotifier.jumpToTrack(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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -56,6 +56,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
useValueListenable(searchController).text,
|
useValueListenable(searchController).text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final controller = useScrollController();
|
||||||
|
|
||||||
final searchRequest = useMemoized(() async {
|
final searchRequest = useMemoized(() async {
|
||||||
if (searchTerm.trim().isEmpty) {
|
if (searchTerm.trim().isEmpty) {
|
||||||
return <YoutubeVideoInfo>[];
|
return <YoutubeVideoInfo>[];
|
||||||
@ -204,8 +206,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]),
|
||||||
@ -223,7 +227,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]),
|
||||||
|
@ -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(
|
controller: controller,
|
||||||
crossAxisMargin: 10,
|
child: child,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,28 +79,25 @@ class GenrePage extends HookConsumerWidget {
|
|||||||
const ShimmerCategories()
|
const ShimmerCategories()
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InterScrollbar(
|
child: ListView.builder(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: ListView.builder(
|
itemCount: categories.length,
|
||||||
controller: scrollController,
|
itemBuilder: (context, index) {
|
||||||
itemCount: categories.length,
|
return AnimatedSwitcher(
|
||||||
itemBuilder: (context, index) {
|
transitionBuilder: (child, animation) {
|
||||||
return AnimatedSwitcher(
|
return FadeTransition(
|
||||||
transitionBuilder: (child, animation) {
|
opacity: animation,
|
||||||
return FadeTransition(
|
child: child,
|
||||||
opacity: animation,
|
);
|
||||||
child: child,
|
},
|
||||||
);
|
duration: const Duration(milliseconds: 300),
|
||||||
},
|
child: searchController.text.isEmpty &&
|
||||||
duration: const Duration(milliseconds: 300),
|
index == categories.length - 1 &&
|
||||||
child: searchController.text.isEmpty &&
|
categoriesQuery.hasNextPage
|
||||||
index == categories.length - 1 &&
|
? const ShimmerCategories()
|
||||||
categoriesQuery.hasNextPage
|
: CategoryCard(categories[index]),
|
||||||
? const ShimmerCategories()
|
);
|
||||||
: CategoryCard(categories[index]),
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,48 +46,45 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
[newReleases.pages],
|
[newReleases.pages],
|
||||||
);
|
);
|
||||||
|
|
||||||
return InterScrollbar(
|
return ListView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: ListView(
|
children: [
|
||||||
controller: controller,
|
if (!featuredPlaylistsQuery.hasPageData &&
|
||||||
children: [
|
!featuredPlaylistsQuery.isLoadingNextPage)
|
||||||
if (!featuredPlaylistsQuery.hasPageData &&
|
const ShimmerCategories()
|
||||||
!featuredPlaylistsQuery.isLoadingNextPage)
|
else
|
||||||
const ShimmerCategories()
|
HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
else
|
items: playlists.toList(),
|
||||||
HorizontalPlaybuttonCardView<PlaylistSimple>(
|
title: Text(context.l10n.featured),
|
||||||
items: playlists.toList(),
|
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
||||||
title: Text(context.l10n.featured),
|
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
||||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
),
|
||||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
if (auth != null &&
|
||||||
),
|
newReleases.hasPageData &&
|
||||||
if (auth != null &&
|
userArtistsQuery.hasData &&
|
||||||
newReleases.hasPageData &&
|
!newReleases.isLoadingNextPage)
|
||||||
userArtistsQuery.hasData &&
|
HorizontalPlaybuttonCardView<Album>(
|
||||||
!newReleases.isLoadingNextPage)
|
items: albums,
|
||||||
HorizontalPlaybuttonCardView<Album>(
|
title: Text(context.l10n.new_releases),
|
||||||
items: albums,
|
hasNextPage: newReleases.hasNextPage,
|
||||||
title: Text(context.l10n.new_releases),
|
onFetchMore: newReleases.fetchNext,
|
||||||
hasNextPage: newReleases.hasNextPage,
|
),
|
||||||
onFetchMore: newReleases.fetchNext,
|
...?madeForUser.data?["content"]?["items"]?.map((item) {
|
||||||
),
|
final playlists = item["content"]?["items"]
|
||||||
...?madeForUser.data?["content"]?["items"]?.map((item) {
|
?.where((itemL2) => itemL2["type"] == "playlist")
|
||||||
final playlists = item["content"]?["items"]
|
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||||
?.where((itemL2) => itemL2["type"] == "playlist")
|
.toList()
|
||||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
.cast<PlaylistSimple>() ??
|
||||||
.toList()
|
<PlaylistSimple>[];
|
||||||
.cast<PlaylistSimple>() ??
|
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||||
<PlaylistSimple>[];
|
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
items: playlists,
|
||||||
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
title: Text(item["name"] ?? ""),
|
||||||
items: playlists,
|
hasNextPage: false,
|
||||||
title: Text(item["name"] ?? ""),
|
onFetchMore: () {},
|
||||||
hasNextPage: false,
|
);
|
||||||
onFetchMore: () {},
|
})
|
||||||
);
|
],
|
||||||
})
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,26 +71,32 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
searchTerm.isNotEmpty;
|
searchTerm.isNotEmpty;
|
||||||
|
|
||||||
final resultWidget = HookBuilder(
|
final resultWidget = HookBuilder(
|
||||||
builder: (context) => InterScrollbar(
|
builder: (context) {
|
||||||
child: SingleChildScrollView(
|
final controller = useScrollController();
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
return InterScrollbar(
|
||||||
child: SafeArea(
|
controller: controller,
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: controller,
|
||||||
children: [
|
child: Padding(
|
||||||
SearchTracksSection(query: searchTrack),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
SearchPlaylistsSection(query: searchPlaylist),
|
child: SafeArea(
|
||||||
const SizedBox(height: 20),
|
child: Column(
|
||||||
SearchArtistsSection(query: searchArtist),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 20),
|
children: [
|
||||||
SearchAlbumsSection(query: searchAlbum),
|
SearchTracksSection(query: searchTrack),
|
||||||
],
|
SearchPlaylistsSection(query: searchPlaylist),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SearchArtistsSection(query: searchArtist),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SearchAlbumsSection(query: searchAlbum),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
@ -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) {
|
||||||
|
@ -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];
|
||||||
|
@ -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,6 +21,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final controller = useScrollController();
|
||||||
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@ -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(),
|
||||||
|
@ -465,6 +465,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:
|
||||||
|
@ -106,6 +106,10 @@ dependencies:
|
|||||||
simple_icons: ^7.10.0
|
simple_icons: ^7.10.0
|
||||||
audio_service_mpris: ^0.1.0
|
audio_service_mpris: ^0.1.0
|
||||||
file_picker: ^6.0.0
|
file_picker: ^6.0.0
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user