feat(mobile): pull to refresh support in all refreshable list views

This commit is contained in:
Kingkor Roy Tirtho 2023-02-03 19:41:23 +06:00
parent 025c1ae204
commit 9f959ce77c
9 changed files with 284 additions and 245 deletions

View File

@ -3,13 +3,15 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
abstract class Env { abstract class Env {
static final String pocketbaseUrl = static final String pocketbaseUrl =
dotenv.get('POCKETBASE_URL', fallback: 'http://localhost:8090'); dotenv.get('POCKETBASE_URL', fallback: 'http://127.0.0.1:8090');
static final String username = dotenv.get('USERNAME', fallback: 'root'); static final String username = dotenv.get('USERNAME', fallback: 'root');
static final String password = dotenv.get('PASSWORD', fallback: '12345678'); static final String password = dotenv.get('PASSWORD', fallback: '12345678');
static configure() async { static configure() async {
if (kReleaseMode) { if (kReleaseMode) {
await dotenv.load(fileName: ".env"); await dotenv.load(fileName: ".env");
} else {
dotenv.testLoad();
} }
} }
} }

View File

@ -62,33 +62,39 @@ class UserAlbums extends HookConsumerWidget {
return const Center(child: ShimmerPlaybuttonCard(count: 7)); return const Center(child: ShimmerPlaybuttonCard(count: 7));
} }
return SingleChildScrollView( return RefreshIndicator(
child: Material( onRefresh: () async {
type: MaterialType.transparency, await albumsQuery.refetch();
textStyle: PlatformTheme.of(context).textTheme!.body!, },
color: PlatformTheme.of(context).scaffoldBackgroundColor, child: SingleChildScrollView(
child: Padding( physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(8.0), child: Material(
child: Column( type: MaterialType.transparency,
children: [ textStyle: PlatformTheme.of(context).textTheme!.body!,
PlatformTextField( color: PlatformTheme.of(context).scaffoldBackgroundColor,
onChanged: (value) => searchText.value = value, child: Padding(
prefixIcon: SpotubeIcons.filter, padding: const EdgeInsets.all(8.0),
placeholder: 'Filter Albums...', child: Column(
), children: [
const SizedBox(height: 20), PlatformTextField(
Wrap( onChanged: (value) => searchText.value = value,
spacing: spacing, // gap between adjacent chips prefixIcon: SpotubeIcons.filter,
runSpacing: 20, // gap between lines placeholder: 'Filter Albums...',
alignment: WrapAlignment.center, ),
children: albums const SizedBox(height: 20),
.map((album) => AlbumCard( Wrap(
viewType: viewType, spacing: spacing, // gap between adjacent chips
TypeConversionUtils.simpleAlbum_X_Album(album), runSpacing: 20, // gap between lines
)) alignment: WrapAlignment.center,
.toList(), children: albums
), .map((album) => AlbumCard(
], viewType: viewType,
TypeConversionUtils.simpleAlbum_X_Album(album),
))
.toList(),
),
],
),
), ),
), ),
), ),

View File

@ -83,30 +83,36 @@ class UserArtists extends HookConsumerWidget {
], ],
), ),
) )
: GridView.builder( : RefreshIndicator(
itemCount: filteredArtists.length, onRefresh: () async {
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( await artistQuery.refetchPages();
maxCrossAxisExtent: 200,
mainAxisExtent: 250,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
return HookBuilder(builder: (context) {
if (index == artistQuery.pages.length - 1 && hasNextPage) {
return Waypoint(
controller: useScrollController(),
isGrid: true,
onTouchEdge: () {
artistQuery.fetchNextPage();
},
child: ArtistCard(filteredArtists[index]),
);
}
return ArtistCard(filteredArtists[index]);
});
}, },
child: GridView.builder(
itemCount: filteredArtists.length,
physics: const AlwaysScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: 250,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
return HookBuilder(builder: (context) {
if (index == artistQuery.pages.length - 1 && hasNextPage) {
return Waypoint(
controller: useScrollController(),
isGrid: true,
onTouchEdge: () {
artistQuery.fetchNextPage();
},
child: ArtistCard(filteredArtists[index]),
);
}
return ArtistCard(filteredArtists[index]);
});
},
),
), ),
); );
} }

View File

@ -20,7 +20,6 @@ import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/hooks/use_async_effect.dart'; import 'package:spotube/hooks/use_async_effect.dart';
import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
@ -162,7 +161,6 @@ class UserLocalTracks extends HookConsumerWidget {
trackSnapshot.value ?? [], trackSnapshot.value ?? [],
); );
final isMounted = useIsMounted(); final isMounted = useIsMounted();
final breakpoint = useBreakpoints();
final searchText = useState<String>(""); final searchText = useState<String>("");
@ -261,28 +259,34 @@ class UserLocalTracks extends HookConsumerWidget {
}, [searchText.value, sortedTracks]); }, [searchText.value, sortedTracks]);
return Expanded( return Expanded(
child: ListView.builder( child: RefreshIndicator(
itemCount: filteredTracks.length, onRefresh: () async {
itemBuilder: (context, index) { ref.refresh(localTracksProvider);
final track = filteredTracks[index];
return TrackTile(
playlist,
duration:
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
track: MapEntry(index, track),
isActive: playlist?.activeTrack.id == track.id,
isChecked: false,
showCheck: false,
isLocal: true,
onTrackPlayButtonPressed: (currentTrack) {
return playLocalTracks(
playlistNotifier,
sortedTracks,
currentTrack: track,
);
},
);
}, },
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
return TrackTile(
playlist,
duration:
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
track: MapEntry(index, track),
isActive: playlist?.activeTrack.id == track.id,
isChecked: false,
showCheck: false,
isLocal: true,
onTrackPlayButtonPressed: (currentTrack) {
return playLocalTracks(
playlistNotifier,
sortedTracks,
currentTrack: track,
);
},
);
},
),
), ),
); );
}, },

View File

@ -89,34 +89,38 @@ class UserPlaylists extends HookConsumerWidget {
)) ))
.toList(), .toList(),
]; ];
return SingleChildScrollView( return RefreshIndicator(
child: Material( onRefresh: () => playlistsQuery.refetch(),
type: MaterialType.transparency, child: SingleChildScrollView(
textStyle: PlatformTheme.of(context).textTheme!.body!, physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Material(
padding: const EdgeInsets.all(8.0), type: MaterialType.transparency,
child: Column( textStyle: PlatformTheme.of(context).textTheme!.body!,
children: [ child: Padding(
PlatformTextField( padding: const EdgeInsets.all(8.0),
onChanged: (value) => searchText.value = value, child: Column(
placeholder: "Filter your playlists...", children: [
prefixIcon: SpotubeIcons.filter, PlatformTextField(
), onChanged: (value) => searchText.value = value,
const SizedBox(height: 20), placeholder: "Filter your playlists...",
if (playlistsQuery.isLoading || !playlistsQuery.hasData) prefixIcon: SpotubeIcons.filter,
const Center(child: ShimmerPlaybuttonCard(count: 7))
else
Center(
child: Wrap(
spacing: spacing, // gap between adjacent chips
runSpacing: 20, // gap between lines
alignment: breakpoint.isSm
? WrapAlignment.center
: WrapAlignment.start,
children: children,
),
), ),
], const SizedBox(height: 20),
if (playlistsQuery.isLoading || !playlistsQuery.hasData)
const Center(child: ShimmerPlaybuttonCard(count: 7))
else
Center(
child: Wrap(
spacing: spacing, // gap between adjacent chips
runSpacing: 20, // gap between lines
alignment: breakpoint.isSm
? WrapAlignment.center
: WrapAlignment.start,
children: children,
),
),
],
),
), ),
), ),
), ),

View File

@ -208,142 +208,150 @@ class TrackCollectionView<T> extends HookConsumerWidget {
), ),
) )
: null, : null,
body: CustomScrollView( body: RefreshIndicator(
controller: controller, onRefresh: () async {
slivers: [ await tracksSnapshot.refetch();
SliverAppBar( },
actions: [ child: CustomScrollView(
if (kIsMobile) controller: controller,
CompactSearch( physics: const AlwaysScrollableScrollPhysics(),
onChanged: (value) => searchText.value = value, slivers: [
placeholder: "Search tracks...", SliverAppBar(
iconColor: color?.titleTextColor, actions: [
), if (kIsMobile)
if (collapsed.value) ...buttons, CompactSearch(
], onChanged: (value) => searchText.value = value,
floating: false, placeholder: "Search tracks...",
pinned: true, iconColor: color?.titleTextColor,
expandedHeight: 400,
automaticallyImplyLeading: kIsMobile,
leading: kIsMobile
? PlatformBackButton(color: color?.titleTextColor)
: null,
iconTheme: IconThemeData(color: color?.titleTextColor),
primary: true,
backgroundColor: color?.color,
title: collapsed.value
? PlatformText.headline(
title,
style: TextStyle(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
)
: null,
centerTitle: true,
flexibleSpace: FlexibleSpaceBar(
background: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color?.color ?? Colors.transparent,
Theme.of(context).canvasColor,
],
begin: const FractionalOffset(0, 0),
end: const FractionalOffset(0, 1),
tileMode: TileMode.clamp,
), ),
), if (collapsed.value) ...buttons,
child: Material( ],
textStyle: PlatformTheme.of(context).textTheme!.body!, floating: false,
type: MaterialType.transparency, pinned: true,
child: Padding( expandedHeight: 400,
padding: const EdgeInsets.symmetric( automaticallyImplyLeading: kIsMobile,
horizontal: 20, leading: kIsMobile
? PlatformBackButton(color: color?.titleTextColor)
: null,
iconTheme: IconThemeData(color: color?.titleTextColor),
primary: true,
backgroundColor: color?.color,
title: collapsed.value
? PlatformText.headline(
title,
style: TextStyle(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
)
: null,
centerTitle: true,
flexibleSpace: FlexibleSpaceBar(
background: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color?.color ?? Colors.transparent,
Theme.of(context).canvasColor,
],
begin: const FractionalOffset(0, 0),
end: const FractionalOffset(0, 1),
tileMode: TileMode.clamp,
), ),
child: Wrap( ),
spacing: 20, child: Material(
runSpacing: 20, textStyle: PlatformTheme.of(context).textTheme!.body!,
crossAxisAlignment: WrapCrossAlignment.center, type: MaterialType.transparency,
alignment: WrapAlignment.center, child: Padding(
runAlignment: WrapAlignment.center, padding: const EdgeInsets.symmetric(
children: [ horizontal: 20,
Container( ),
constraints: const BoxConstraints(maxHeight: 200), child: Wrap(
child: ClipRRect( spacing: 20,
borderRadius: BorderRadius.circular(10), runSpacing: 20,
child: UniversalImage( crossAxisAlignment: WrapCrossAlignment.center,
path: titleImage, alignment: WrapAlignment.center,
placeholder: (context, url) { runAlignment: WrapAlignment.center,
return Assets.albumPlaceholder.image(); children: [
}, Container(
constraints:
const BoxConstraints(maxHeight: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
path: titleImage,
placeholder: (context, url) {
return Assets.albumPlaceholder.image();
},
),
), ),
), ),
), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment:
mainAxisAlignment: MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
PlatformText.headline( PlatformText.headline(
title, title,
style: TextStyle(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
),
if (description != null)
PlatformText(
description!,
style: TextStyle( style: TextStyle(
color: color?.bodyTextColor, color: color?.titleTextColor,
fontWeight: FontWeight.w600,
), ),
maxLines: 2,
overflow: TextOverflow.fade,
), ),
const SizedBox(height: 10), if (description != null)
Row( PlatformText(
mainAxisSize: MainAxisSize.min, description!,
children: buttons, style: TextStyle(
), color: color?.bodyTextColor,
], ),
) maxLines: 2,
], overflow: TextOverflow.fade,
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: buttons,
),
],
)
],
),
), ),
), ),
), ),
), ),
), ),
), HookBuilder(
HookBuilder( builder: (context) {
builder: (context) { if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { return const ShimmerTrackTile();
return const ShimmerTrackTile(); } else if (tracksSnapshot.hasError &&
} else if (tracksSnapshot.hasError && tracksSnapshot.isError) {
tracksSnapshot.isError) { return SliverToBoxAdapter(
return SliverToBoxAdapter( child: PlatformText("Error ${tracksSnapshot.error}"));
child: PlatformText("Error ${tracksSnapshot.error}")); }
}
return TracksTableView( return TracksTableView(
List.from( List.from(
(filteredTracks ?? []).map( (filteredTracks ?? []).map(
(e) { (e) {
if (e is Track) { if (e is Track) {
return e; return e;
} else { } else {
return TypeConversionUtils.simpleTrack_X_Track( return TypeConversionUtils.simpleTrack_X_Track(
e, album!); e, album!);
} }
}, },
),
), ),
), onTrackPlayButtonPressed: onPlay,
onTrackPlayButtonPressed: onPlay, playlistId: id,
playlistId: id, userPlaylist: isOwned,
userPlaylist: isOwned, );
); },
}, )
) ],
], ),
)), )),
); );
} }

View File

@ -39,11 +39,12 @@ void main(List<String> rawArgs) async {
'verbose', 'verbose',
abbr: 'v', abbr: 'v',
help: 'Verbose mode', help: 'Verbose mode',
defaultsTo: !kReleaseMode,
callback: (verbose) { callback: (verbose) {
if (verbose) { if (verbose) {
Platform.environment['VERBOSE'] = 'true'; logEnv['VERBOSE'] = 'true';
Platform.environment['DEBUG'] = 'true'; logEnv['DEBUG'] = 'true';
Platform.environment['ERROR'] = 'true'; logEnv['ERROR'] = 'true';
} }
}, },
); );
@ -102,7 +103,7 @@ void main(List<String> rawArgs) async {
} }
Catcher( Catcher(
enableLogger: arguments["verbose"] ?? !kReleaseMode, enableLogger: arguments["verbose"],
debugConfig: CatcherOptions( debugConfig: CatcherOptions(
SilentReportMode(), SilentReportMode(),
[ [

View File

@ -7,6 +7,9 @@ import 'package:path/path.dart' as path;
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
final _loggerFactory = SpotubeLogger(); final _loggerFactory = SpotubeLogger();
final logEnv = {
if (!kIsWeb) ...Platform.environment,
};
SpotubeLogger getLogger<T>(T owner) { SpotubeLogger getLogger<T>(T owner) {
_loggerFactory.owner = owner is String ? owner : owner.toString(); _loggerFactory.owner = owner is String ? owner : owner.toString();
@ -60,10 +63,9 @@ class SpotubeLogger extends Logger {
class _SpotubeLogFilter extends DevelopmentFilter { class _SpotubeLogFilter extends DevelopmentFilter {
@override @override
bool shouldLog(LogEvent event) { bool shouldLog(LogEvent event) {
final env = kIsWeb ? {} : Platform.environment; if ((logEnv["DEBUG"] == "true" && event.level == Level.debug) ||
if ((env["DEBUG"] == "true" && event.level == Level.debug) || (logEnv["VERBOSE"] == "true" && event.level == Level.verbose) ||
(env["VERBOSE"] == "true" && event.level == Level.verbose) || (logEnv["ERROR"] == "true" && event.level == Level.error)) {
(env["ERROR"] == "true" && event.level == Level.error)) {
return true; return true;
} }
return super.shouldLog(event); return super.shouldLog(event);

View File

@ -92,23 +92,29 @@ class GenrePage extends HookConsumerWidget {
placeholder: "Filter categories or genres...", placeholder: "Filter categories or genres...",
); );
final list = Waypoint( final list = RefreshIndicator(
onTouchEdge: () async { onRefresh: () async {
if (categoriesQuery.hasNextPage && isMounted()) { await categoriesQuery.refetchPages();
await categoriesQuery.fetchNextPage();
}
}, },
controller: scrollController, child: Waypoint(
child: ListView.builder( onTouchEdge: () async {
controller: scrollController, if (categoriesQuery.hasNextPage && isMounted()) {
itemCount: categories.length, await categoriesQuery.fetchNextPage();
itemBuilder: (context, index) {
final category = categories[index];
if (searchText.value.isEmpty && index == categories.length - 1) {
return const ShimmerCategories();
} }
return CategoryCard(category);
}, },
controller: scrollController,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
if (searchText.value.isEmpty && index == categories.length - 1) {
return const ShimmerCategories();
}
return CategoryCard(category);
},
),
), ),
); );
return PlatformScaffold( return PlatformScaffold(