diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 2d3965f48..7921c684c 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -3,13 +3,15 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; abstract class Env { 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 password = dotenv.get('PASSWORD', fallback: '12345678'); static configure() async { if (kReleaseMode) { await dotenv.load(fileName: ".env"); + } else { + dotenv.testLoad(); } } } diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 4f6612829..4d914ed2c 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -62,33 +62,39 @@ class UserAlbums extends HookConsumerWidget { return const Center(child: ShimmerPlaybuttonCard(count: 7)); } - return SingleChildScrollView( - child: Material( - type: MaterialType.transparency, - textStyle: PlatformTheme.of(context).textTheme!.body!, - color: PlatformTheme.of(context).scaffoldBackgroundColor, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - PlatformTextField( - onChanged: (value) => searchText.value = value, - prefixIcon: SpotubeIcons.filter, - placeholder: 'Filter Albums...', - ), - const SizedBox(height: 20), - Wrap( - spacing: spacing, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: WrapAlignment.center, - children: albums - .map((album) => AlbumCard( - viewType: viewType, - TypeConversionUtils.simpleAlbum_X_Album(album), - )) - .toList(), - ), - ], + return RefreshIndicator( + onRefresh: () async { + await albumsQuery.refetch(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Material( + type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, + color: PlatformTheme.of(context).scaffoldBackgroundColor, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + PlatformTextField( + onChanged: (value) => searchText.value = value, + prefixIcon: SpotubeIcons.filter, + placeholder: 'Filter Albums...', + ), + const SizedBox(height: 20), + Wrap( + spacing: spacing, // gap between adjacent chips + runSpacing: 20, // gap between lines + alignment: WrapAlignment.center, + children: albums + .map((album) => AlbumCard( + viewType: viewType, + TypeConversionUtils.simpleAlbum_X_Album(album), + )) + .toList(), + ), + ], + ), ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index f2af67fb8..0b6ab4087 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -83,30 +83,36 @@ class UserArtists extends HookConsumerWidget { ], ), ) - : GridView.builder( - itemCount: filteredArtists.length, - 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]); - }); + : RefreshIndicator( + onRefresh: () async { + await artistQuery.refetchPages(); }, + 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]); + }); + }, + ), ), ); } diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 61b5666f5..9db22b928 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -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/track_table/track_tile.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/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; @@ -162,7 +161,6 @@ class UserLocalTracks extends HookConsumerWidget { trackSnapshot.value ?? [], ); final isMounted = useIsMounted(); - final breakpoint = useBreakpoints(); final searchText = useState(""); @@ -261,28 +259,34 @@ class UserLocalTracks extends HookConsumerWidget { }, [searchText.value, sortedTracks]); return Expanded( - child: ListView.builder( - 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, - ); - }, - ); + child: RefreshIndicator( + onRefresh: () async { + ref.refresh(localTracksProvider); }, + 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, + ); + }, + ); + }, + ), ), ); }, diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 439f7d632..cf01792de 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -89,34 +89,38 @@ class UserPlaylists extends HookConsumerWidget { )) .toList(), ]; - return SingleChildScrollView( - child: Material( - type: MaterialType.transparency, - textStyle: PlatformTheme.of(context).textTheme!.body!, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - PlatformTextField( - onChanged: (value) => searchText.value = value, - placeholder: "Filter your playlists...", - prefixIcon: SpotubeIcons.filter, - ), - 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, - ), + return RefreshIndicator( + onRefresh: () => playlistsQuery.refetch(), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Material( + type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + PlatformTextField( + onChanged: (value) => searchText.value = value, + placeholder: "Filter your playlists...", + prefixIcon: SpotubeIcons.filter, ), - ], + 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, + ), + ), + ], + ), ), ), ), diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index b85987cce..7e8c3bab6 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -208,142 +208,150 @@ class TrackCollectionView extends HookConsumerWidget { ), ) : null, - body: CustomScrollView( - controller: controller, - slivers: [ - SliverAppBar( - actions: [ - if (kIsMobile) - CompactSearch( - onChanged: (value) => searchText.value = value, - placeholder: "Search tracks...", - iconColor: color?.titleTextColor, - ), - if (collapsed.value) ...buttons, - ], - floating: false, - pinned: true, - 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, + body: RefreshIndicator( + onRefresh: () async { + await tracksSnapshot.refetch(); + }, + child: CustomScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverAppBar( + actions: [ + if (kIsMobile) + CompactSearch( + onChanged: (value) => searchText.value = value, + placeholder: "Search tracks...", + iconColor: color?.titleTextColor, ), - ), - child: Material( - textStyle: PlatformTheme.of(context).textTheme!.body!, - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, + if (collapsed.value) ...buttons, + ], + floating: false, + pinned: true, + 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, ), - child: Wrap( - spacing: 20, - runSpacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - 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( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PlatformText.headline( - title, - style: TextStyle( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, + ), + child: Material( + textStyle: PlatformTheme.of(context).textTheme!.body!, + type: MaterialType.transparency, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Wrap( + spacing: 20, + runSpacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + Container( + constraints: + const BoxConstraints(maxHeight: 200), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: titleImage, + placeholder: (context, url) { + return Assets.albumPlaceholder.image(); + }, ), ), - if (description != null) - PlatformText( - description!, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + PlatformText.headline( + title, style: TextStyle( - color: color?.bodyTextColor, + color: color?.titleTextColor, + fontWeight: FontWeight.w600, ), - maxLines: 2, - overflow: TextOverflow.fade, ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), - ], - ) - ], + if (description != null) + PlatformText( + description!, + style: TextStyle( + color: color?.bodyTextColor, + ), + maxLines: 2, + overflow: TextOverflow.fade, + ), + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + children: buttons, + ), + ], + ) + ], + ), ), ), ), ), ), - ), - HookBuilder( - builder: (context) { - if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { - return const ShimmerTrackTile(); - } else if (tracksSnapshot.hasError && - tracksSnapshot.isError) { - return SliverToBoxAdapter( - child: PlatformText("Error ${tracksSnapshot.error}")); - } + HookBuilder( + builder: (context) { + if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { + return const ShimmerTrackTile(); + } else if (tracksSnapshot.hasError && + tracksSnapshot.isError) { + return SliverToBoxAdapter( + child: PlatformText("Error ${tracksSnapshot.error}")); + } - return TracksTableView( - List.from( - (filteredTracks ?? []).map( - (e) { - if (e is Track) { - return e; - } else { - return TypeConversionUtils.simpleTrack_X_Track( - e, album!); - } - }, + return TracksTableView( + List.from( + (filteredTracks ?? []).map( + (e) { + if (e is Track) { + return e; + } else { + return TypeConversionUtils.simpleTrack_X_Track( + e, album!); + } + }, + ), ), - ), - onTrackPlayButtonPressed: onPlay, - playlistId: id, - userPlaylist: isOwned, - ); - }, - ) - ], + onTrackPlayButtonPressed: onPlay, + playlistId: id, + userPlaylist: isOwned, + ); + }, + ) + ], + ), )), ); } diff --git a/lib/main.dart b/lib/main.dart index ce5fba436..627e18438 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,11 +39,12 @@ void main(List rawArgs) async { 'verbose', abbr: 'v', help: 'Verbose mode', + defaultsTo: !kReleaseMode, callback: (verbose) { if (verbose) { - Platform.environment['VERBOSE'] = 'true'; - Platform.environment['DEBUG'] = 'true'; - Platform.environment['ERROR'] = 'true'; + logEnv['VERBOSE'] = 'true'; + logEnv['DEBUG'] = 'true'; + logEnv['ERROR'] = 'true'; } }, ); @@ -102,7 +103,7 @@ void main(List rawArgs) async { } Catcher( - enableLogger: arguments["verbose"] ?? !kReleaseMode, + enableLogger: arguments["verbose"], debugConfig: CatcherOptions( SilentReportMode(), [ diff --git a/lib/models/logger.dart b/lib/models/logger.dart index 9237c818c..d41991739 100644 --- a/lib/models/logger.dart +++ b/lib/models/logger.dart @@ -7,6 +7,9 @@ import 'package:path/path.dart' as path; import 'package:spotube/utils/platform.dart'; final _loggerFactory = SpotubeLogger(); +final logEnv = { + if (!kIsWeb) ...Platform.environment, +}; SpotubeLogger getLogger(T owner) { _loggerFactory.owner = owner is String ? owner : owner.toString(); @@ -60,10 +63,9 @@ class SpotubeLogger extends Logger { class _SpotubeLogFilter extends DevelopmentFilter { @override bool shouldLog(LogEvent event) { - final env = kIsWeb ? {} : Platform.environment; - if ((env["DEBUG"] == "true" && event.level == Level.debug) || - (env["VERBOSE"] == "true" && event.level == Level.verbose) || - (env["ERROR"] == "true" && event.level == Level.error)) { + if ((logEnv["DEBUG"] == "true" && event.level == Level.debug) || + (logEnv["VERBOSE"] == "true" && event.level == Level.verbose) || + (logEnv["ERROR"] == "true" && event.level == Level.error)) { return true; } return super.shouldLog(event); diff --git a/lib/pages/genre/genres.dart b/lib/pages/genre/genres.dart index 32dabf6d6..09035ba84 100644 --- a/lib/pages/genre/genres.dart +++ b/lib/pages/genre/genres.dart @@ -92,23 +92,29 @@ class GenrePage extends HookConsumerWidget { placeholder: "Filter categories or genres...", ); - final list = Waypoint( - onTouchEdge: () async { - if (categoriesQuery.hasNextPage && isMounted()) { - await categoriesQuery.fetchNextPage(); - } + final list = RefreshIndicator( + onRefresh: () async { + await categoriesQuery.refetchPages(); }, - controller: scrollController, - child: ListView.builder( - controller: scrollController, - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - if (searchText.value.isEmpty && index == categories.length - 1) { - return const ShimmerCategories(); + child: Waypoint( + onTouchEdge: () async { + if (categoriesQuery.hasNextPage && isMounted()) { + await categoriesQuery.fetchNextPage(); } - 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(