From ba7505c2b382dde7eb4e00504c43e9b14897ea26 Mon Sep 17 00:00:00 2001 From: Flummi Date: Tue, 24 Jun 2025 03:02:39 +0200 Subject: [PATCH] v1.4.8+69 --- lib/controller/mediacontroller.dart | 37 +++--- lib/screens/mediagrid.dart | 184 +++++++++++++++++++--------- lib/widgets/filter_bar.dart | 6 +- pubspec.yaml | 2 +- 4 files changed, 144 insertions(+), 85 deletions(-) diff --git a/lib/controller/mediacontroller.dart b/lib/controller/mediacontroller.dart index 0dc8f9f..f619b61 100644 --- a/lib/controller/mediacontroller.dart +++ b/lib/controller/mediacontroller.dart @@ -14,6 +14,7 @@ class MediaController extends GetxController { RxBool loading = false.obs; RxBool atEnd = false.obs; RxBool atStart = false.obs; + Rxn errorMessage = Rxn(); RxInt typeIndex = 0.obs; RxInt modeIndex = 0.obs; @@ -22,12 +23,10 @@ class MediaController extends GetxController { void setTypeIndex(int idx) { typeIndex.value = idx; - fetchInitial(); } void setModeIndex(int idx) { modeIndex.value = idx; - fetchInitial(); } void setTag(String? newTag, {bool reload = true}) { @@ -37,6 +36,10 @@ class MediaController extends GetxController { } } + void toggleRandom() { + random.value = random.value == 0 ? 1 : 0; + } + Future?> toggleFavorite( MediaItem item, bool isFavorite, @@ -51,6 +54,7 @@ class MediaController extends GetxController { Future _fetchItems({int? older, int? newer}) async { if (loading.value) return null; loading.value = true; + errorMessage.value = null; try { return await _api.fetchItems( older: older, @@ -61,11 +65,10 @@ class MediaController extends GetxController { tag: tag.value, ); } catch (e) { - Get.snackbar( - 'Fehler beim Laden', - 'Die Daten konnten nicht abgerufen werden. Wo Internet?', - snackPosition: SnackPosition.BOTTOM, - ); + final String errorText = + 'Die Daten konnten nicht abgerufen werden. Wo Internet?'; + errorMessage.value = errorText; + Get.snackbar('Fehler beim Laden', errorText); return null; } finally { loading.value = false; @@ -73,7 +76,7 @@ class MediaController extends GetxController { } Future fetchInitial({int? id}) async { - final result = await _fetchItems(older: id); + final Feed? result = await _fetchItems(older: id); if (result != null) { items.assignAll(result.items); atEnd.value = result.atEnd; @@ -83,7 +86,7 @@ class MediaController extends GetxController { Future fetchMore() async { if (items.isEmpty || atEnd.value) return; - final result = await _fetchItems(older: items.last.id); + final Feed? result = await _fetchItems(older: items.last.id); if (result != null) { final Set existingIds = items.map((e) => e.id).toSet(); final List newItems = result.items @@ -95,10 +98,9 @@ class MediaController extends GetxController { } } - Future fetchNewer() async { - if (items.isEmpty || atStart.value) return 0; - final oldLength = items.length; - final result = await _fetchItems(newer: items.first.id); + Future fetchNewer() async { + if (items.isEmpty || atStart.value) return; + final Feed? result = await _fetchItems(newer: items.first.id); if (result != null) { final Set existingIds = items.map((e) => e.id).toSet(); final List newItems = result.items @@ -107,9 +109,8 @@ class MediaController extends GetxController { items.insertAll(0, newItems); items.refresh(); atStart.value = result.atStart; - return items.length - oldLength; } - return 0; + return; } Future handleRefresh() async { @@ -122,15 +123,11 @@ class MediaController extends GetxController { } Future handleLoading() async { + if (loading.value) return; if (!loading.value && !atEnd.value) { await fetchMore(); } } - void toggleRandom() { - random.value = random.value == 1 ? 0 : 1; - fetchInitial(); - } - bool get isRandomEnabled => random.value == 1; } diff --git a/lib/screens/mediagrid.dart b/lib/screens/mediagrid.dart index 3dba008..678bb42 100644 --- a/lib/screens/mediagrid.dart +++ b/lib/screens/mediagrid.dart @@ -29,11 +29,27 @@ class _MediaGrid extends State { late final _MediaGridAppBar _appBar; late final _MediaGridBody _body; + Worker? _filterWorker; @override void initState() { super.initState(); _mediaController.fetchInitial(); + + _filterWorker = everAll( + [ + _mediaController.typeIndex, + _mediaController.modeIndex, + _mediaController.tag, + _mediaController.random, + ], + (_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _refreshController.requestRefresh(); + }); + }, + ); + _appBar = _MediaGridAppBar(mediaController: _mediaController); _body = _MediaGridBody( refreshController: _refreshController, @@ -45,6 +61,7 @@ class _MediaGrid extends State { @override void dispose() { + _filterWorker?.dispose(); _scrollController.dispose(); _refreshController.dispose(); super.dispose(); @@ -52,19 +69,55 @@ class _MediaGrid extends State { @override Widget build(BuildContext context) { - return Obx( - () => Scaffold( + return Obx(() { + if (_mediaController.loading.value && _mediaController.items.isEmpty) { + return Scaffold( + appBar: _appBar, + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (_mediaController.errorMessage.value != null && + _mediaController.items.isEmpty) { + return Scaffold( + appBar: _appBar, + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 60), + const SizedBox(height: 16), + Text( + '${_mediaController.errorMessage.value}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _mediaController.fetchInitial(), + child: const Text('Erneut versuchen'), + ), + ], + ), + ), + ), + ); + } + + return Scaffold( endDrawer: const EndDrawer(), endDrawerEnableOpenDragGesture: _settingsController.drawerSwipeEnabled.value, - bottomNavigationBar: FilterBar(scrollController: _scrollController), + bottomNavigationBar: FilterBar(), appBar: _appBar, body: _body, persistentFooterButtons: _mediaController.tag.value != null ? [TagFooter()] : null, - ), - ); + ); + }); } } @@ -78,7 +131,9 @@ class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( title: InkWell( onTap: () { - mediaController.setTag(null); + if (mediaController.tag.value != null) { + mediaController.setTag(null); + } }, child: Row( mainAxisSize: MainAxisSize.min, @@ -106,7 +161,9 @@ class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget { ? Icons.shuffle_on_outlined : Icons.shuffle, ), - onPressed: mediaController.toggleRandom, + onPressed: () { + mediaController.toggleRandom(); + }, ), ), IconButton( @@ -134,63 +191,72 @@ class _MediaGridBody extends StatelessWidget { final SettingsController settingsController; final ScrollController scrollController; - @override Widget build(BuildContext context) { - return PullexRefresh( - controller: refreshController, - enablePullDown: true, - enablePullUp: true, - header: const WaterDropHeader(), - onRefresh: () async { - try { - await mediaController.handleRefresh(); - } finally { - refreshController.refreshCompleted(); + if (mediaController.items.isEmpty && !mediaController.loading.value) { + return const Center( + child: Text( + 'Keine f0cks gefunden.\n\nVersuch mal andere Filter.', + textAlign: TextAlign.center, + ), + ); + } + return NotificationListener( + onNotification: (scrollInfo) { + if (!mediaController.loading.value && + !mediaController.atEnd.value && + scrollInfo.metrics.pixels >= + scrollInfo.metrics.maxScrollExtent - 600) { + mediaController.handleLoading(); } + return true; }, - onLoading: () async { - try { - await mediaController.handleLoading(); - } finally { - refreshController.loadComplete(); - } - }, - child: Obx( - () => GridView.builder( - addAutomaticKeepAlives: false, - controller: scrollController, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: const EdgeInsets.all(4), - itemCount: mediaController.items.length, - gridDelegate: settingsController.crossAxisCount.value == 0 - ? const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 150, - crossAxisSpacing: 5, - mainAxisSpacing: 5, - childAspectRatio: 1, - ) - : SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: settingsController.crossAxisCount.value, - crossAxisSpacing: 5, - mainAxisSpacing: 5, - childAspectRatio: 1, + child: PullexRefresh( + controller: refreshController, + onRefresh: () async { + try { + await mediaController.handleRefresh(); + } finally { + refreshController.refreshCompleted(); + } + }, + header: const WaterDropHeader(), + child: Obx( + () => GridView.builder( + addAutomaticKeepAlives: false, + controller: scrollController, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(4), + itemCount: mediaController.items.length, + gridDelegate: settingsController.crossAxisCount.value == 0 + ? const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + childAspectRatio: 1, + ) + : SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: settingsController.crossAxisCount.value, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + childAspectRatio: 1, + ), + itemBuilder: (context, index) { + final MediaItem item = mediaController.items[index]; + return Hero( + tag: 'media_${item.id}', + child: Material( + type: MaterialType.transparency, + child: GestureDetector( + key: ValueKey(item.id), + onTap: () => Get.toNamed('/${item.id}'), + child: MediaTile(item: item), + ), ), - itemBuilder: (context, index) { - final MediaItem item = mediaController.items[index]; - return Hero( - tag: 'media_${item.id}', - child: Material( - type: MaterialType.transparency, - child: GestureDetector( - key: ValueKey(item.id), - onTap: () => Get.toNamed('/${item.id}'), - child: MediaTile(item: item), - ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/widgets/filter_bar.dart b/lib/widgets/filter_bar.dart index 5121039..b74378e 100644 --- a/lib/widgets/filter_bar.dart +++ b/lib/widgets/filter_bar.dart @@ -5,9 +5,7 @@ import 'package:get/get.dart'; import 'package:f0ckapp/controller/mediacontroller.dart'; class FilterBar extends StatelessWidget { - final ScrollController scrollController; - - const FilterBar({super.key, required this.scrollController}); + const FilterBar({super.key}); @override Widget build(BuildContext context) { @@ -32,7 +30,6 @@ class FilterBar extends StatelessWidget { onChanged: (String? newValue) { if (newValue != null) { c.setTypeIndex(mediaTypes.indexOf(newValue)); - scrollController.jumpTo(0); } }, ), @@ -51,7 +48,6 @@ class FilterBar extends StatelessWidget { onChanged: (String? newValue) { if (newValue != null) { c.setModeIndex(mediaModes.indexOf(newValue)); - scrollController.jumpTo(0); } }, ), diff --git a/pubspec.yaml b/pubspec.yaml index 6a402f1..4e764d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.4.7+68 +version: 1.4.8+69 environment: sdk: ^3.9.0-100.2.beta