feat(android): better quick scroll/drag to scroll implementation

This commit is contained in:
Kingkor Roy Tirtho 2023-11-13 23:17:16 +06:00
parent 0a6b54da36
commit 2e2c44f0af
13 changed files with 330 additions and 340 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,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]);
},
)
],
), ),
), ),
); );

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,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);
},
),
);
},
),
), ),
), ),
], ),
); ],
}), ),
), ),
), ),
), ),

View File

@ -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]),

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( 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,
),
); );
} }
} }

View File

@ -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]), },
);
},
),
), ),
), ),
], ],

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,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: () {}, })
); ],
})
],
),
); );
} }
} }

View File

@ -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(

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

@ -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(),

View File

@ -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:

View File

@ -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