v1.4.8+69
All checks were successful
Flutter Schmutter / build (push) Successful in 3m39s

This commit is contained in:
2025-06-24 03:02:39 +02:00
parent 39fadc009f
commit ba7505c2b3
4 changed files with 144 additions and 85 deletions

View File

@ -14,6 +14,7 @@ class MediaController extends GetxController {
RxBool loading = false.obs; RxBool loading = false.obs;
RxBool atEnd = false.obs; RxBool atEnd = false.obs;
RxBool atStart = false.obs; RxBool atStart = false.obs;
Rxn<String> errorMessage = Rxn<String>();
RxInt typeIndex = 0.obs; RxInt typeIndex = 0.obs;
RxInt modeIndex = 0.obs; RxInt modeIndex = 0.obs;
@ -22,12 +23,10 @@ class MediaController extends GetxController {
void setTypeIndex(int idx) { void setTypeIndex(int idx) {
typeIndex.value = idx; typeIndex.value = idx;
fetchInitial();
} }
void setModeIndex(int idx) { void setModeIndex(int idx) {
modeIndex.value = idx; modeIndex.value = idx;
fetchInitial();
} }
void setTag(String? newTag, {bool reload = true}) { 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<List<Favorite>?> toggleFavorite( Future<List<Favorite>?> toggleFavorite(
MediaItem item, MediaItem item,
bool isFavorite, bool isFavorite,
@ -51,6 +54,7 @@ class MediaController extends GetxController {
Future<Feed?> _fetchItems({int? older, int? newer}) async { Future<Feed?> _fetchItems({int? older, int? newer}) async {
if (loading.value) return null; if (loading.value) return null;
loading.value = true; loading.value = true;
errorMessage.value = null;
try { try {
return await _api.fetchItems( return await _api.fetchItems(
older: older, older: older,
@ -61,11 +65,10 @@ class MediaController extends GetxController {
tag: tag.value, tag: tag.value,
); );
} catch (e) { } catch (e) {
Get.snackbar( final String errorText =
'Fehler beim Laden', 'Die Daten konnten nicht abgerufen werden. Wo Internet?';
'Die Daten konnten nicht abgerufen werden. Wo Internet?', errorMessage.value = errorText;
snackPosition: SnackPosition.BOTTOM, Get.snackbar('Fehler beim Laden', errorText);
);
return null; return null;
} finally { } finally {
loading.value = false; loading.value = false;
@ -73,7 +76,7 @@ class MediaController extends GetxController {
} }
Future<void> fetchInitial({int? id}) async { Future<void> fetchInitial({int? id}) async {
final result = await _fetchItems(older: id); final Feed? result = await _fetchItems(older: id);
if (result != null) { if (result != null) {
items.assignAll(result.items); items.assignAll(result.items);
atEnd.value = result.atEnd; atEnd.value = result.atEnd;
@ -83,7 +86,7 @@ class MediaController extends GetxController {
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (items.isEmpty || atEnd.value) return; 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) { if (result != null) {
final Set<int> existingIds = items.map((e) => e.id).toSet(); final Set<int> existingIds = items.map((e) => e.id).toSet();
final List<MediaItem> newItems = result.items final List<MediaItem> newItems = result.items
@ -95,10 +98,9 @@ class MediaController extends GetxController {
} }
} }
Future<int> fetchNewer() async { Future<void> fetchNewer() async {
if (items.isEmpty || atStart.value) return 0; if (items.isEmpty || atStart.value) return;
final oldLength = items.length; final Feed? result = await _fetchItems(newer: items.first.id);
final result = await _fetchItems(newer: items.first.id);
if (result != null) { if (result != null) {
final Set<int> existingIds = items.map((e) => e.id).toSet(); final Set<int> existingIds = items.map((e) => e.id).toSet();
final List<MediaItem> newItems = result.items final List<MediaItem> newItems = result.items
@ -107,9 +109,8 @@ class MediaController extends GetxController {
items.insertAll(0, newItems); items.insertAll(0, newItems);
items.refresh(); items.refresh();
atStart.value = result.atStart; atStart.value = result.atStart;
return items.length - oldLength;
} }
return 0; return;
} }
Future<void> handleRefresh() async { Future<void> handleRefresh() async {
@ -122,15 +123,11 @@ class MediaController extends GetxController {
} }
Future<void> handleLoading() async { Future<void> handleLoading() async {
if (loading.value) return;
if (!loading.value && !atEnd.value) { if (!loading.value && !atEnd.value) {
await fetchMore(); await fetchMore();
} }
} }
void toggleRandom() {
random.value = random.value == 1 ? 0 : 1;
fetchInitial();
}
bool get isRandomEnabled => random.value == 1; bool get isRandomEnabled => random.value == 1;
} }

View File

@ -29,11 +29,27 @@ class _MediaGrid extends State<MediaGrid> {
late final _MediaGridAppBar _appBar; late final _MediaGridAppBar _appBar;
late final _MediaGridBody _body; late final _MediaGridBody _body;
Worker? _filterWorker;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_mediaController.fetchInitial(); _mediaController.fetchInitial();
_filterWorker = everAll(
[
_mediaController.typeIndex,
_mediaController.modeIndex,
_mediaController.tag,
_mediaController.random,
],
(_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_refreshController.requestRefresh();
});
},
);
_appBar = _MediaGridAppBar(mediaController: _mediaController); _appBar = _MediaGridAppBar(mediaController: _mediaController);
_body = _MediaGridBody( _body = _MediaGridBody(
refreshController: _refreshController, refreshController: _refreshController,
@ -45,6 +61,7 @@ class _MediaGrid extends State<MediaGrid> {
@override @override
void dispose() { void dispose() {
_filterWorker?.dispose();
_scrollController.dispose(); _scrollController.dispose();
_refreshController.dispose(); _refreshController.dispose();
super.dispose(); super.dispose();
@ -52,19 +69,55 @@ class _MediaGrid extends State<MediaGrid> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx( return Obx(() {
() => Scaffold( 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(), endDrawer: const EndDrawer(),
endDrawerEnableOpenDragGesture: endDrawerEnableOpenDragGesture:
_settingsController.drawerSwipeEnabled.value, _settingsController.drawerSwipeEnabled.value,
bottomNavigationBar: FilterBar(scrollController: _scrollController), bottomNavigationBar: FilterBar(),
appBar: _appBar, appBar: _appBar,
body: _body, body: _body,
persistentFooterButtons: _mediaController.tag.value != null persistentFooterButtons: _mediaController.tag.value != null
? [TagFooter()] ? [TagFooter()]
: null, : null,
),
); );
});
} }
} }
@ -78,7 +131,9 @@ class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget {
return AppBar( return AppBar(
title: InkWell( title: InkWell(
onTap: () { onTap: () {
if (mediaController.tag.value != null) {
mediaController.setTag(null); mediaController.setTag(null);
}
}, },
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -106,7 +161,9 @@ class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget {
? Icons.shuffle_on_outlined ? Icons.shuffle_on_outlined
: Icons.shuffle, : Icons.shuffle,
), ),
onPressed: mediaController.toggleRandom, onPressed: () {
mediaController.toggleRandom();
},
), ),
), ),
IconButton( IconButton(
@ -134,14 +191,28 @@ class _MediaGridBody extends StatelessWidget {
final SettingsController settingsController; final SettingsController settingsController;
final ScrollController scrollController; final ScrollController scrollController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PullexRefresh( 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<ScrollNotification>(
onNotification: (scrollInfo) {
if (!mediaController.loading.value &&
!mediaController.atEnd.value &&
scrollInfo.metrics.pixels >=
scrollInfo.metrics.maxScrollExtent - 600) {
mediaController.handleLoading();
}
return true;
},
child: PullexRefresh(
controller: refreshController, controller: refreshController,
enablePullDown: true,
enablePullUp: true,
header: const WaterDropHeader(),
onRefresh: () async { onRefresh: () async {
try { try {
await mediaController.handleRefresh(); await mediaController.handleRefresh();
@ -149,19 +220,13 @@ class _MediaGridBody extends StatelessWidget {
refreshController.refreshCompleted(); refreshController.refreshCompleted();
} }
}, },
onLoading: () async { header: const WaterDropHeader(),
try {
await mediaController.handleLoading();
} finally {
refreshController.loadComplete();
}
},
child: Obx( child: Obx(
() => GridView.builder( () => GridView.builder(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
controller: scrollController, controller: scrollController,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
itemCount: mediaController.items.length, itemCount: mediaController.items.length,
gridDelegate: settingsController.crossAxisCount.value == 0 gridDelegate: settingsController.crossAxisCount.value == 0
@ -193,6 +258,7 @@ class _MediaGridBody extends StatelessWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -5,9 +5,7 @@ import 'package:get/get.dart';
import 'package:f0ckapp/controller/mediacontroller.dart'; import 'package:f0ckapp/controller/mediacontroller.dart';
class FilterBar extends StatelessWidget { class FilterBar extends StatelessWidget {
final ScrollController scrollController; const FilterBar({super.key});
const FilterBar({super.key, required this.scrollController});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,7 +30,6 @@ class FilterBar extends StatelessWidget {
onChanged: (String? newValue) { onChanged: (String? newValue) {
if (newValue != null) { if (newValue != null) {
c.setTypeIndex(mediaTypes.indexOf(newValue)); c.setTypeIndex(mediaTypes.indexOf(newValue));
scrollController.jumpTo(0);
} }
}, },
), ),
@ -51,7 +48,6 @@ class FilterBar extends StatelessWidget {
onChanged: (String? newValue) { onChanged: (String? newValue) {
if (newValue != null) { if (newValue != null) {
c.setModeIndex(mediaModes.indexOf(newValue)); c.setModeIndex(mediaModes.indexOf(newValue));
scrollController.jumpTo(0);
} }
}, },
), ),

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta