From e38d2086b35f9fa2495253b99daf228807af4d5d Mon Sep 17 00:00:00 2001 From: Flummi Date: Wed, 11 Jun 2025 18:56:55 +0200 Subject: [PATCH] v1.1.20+50 --- lib/providers/media_provider.dart | 16 +- lib/screens/detailview_screen.dart | 22 +- lib/screens/mediagrid_screen.dart | 345 ++++++++--------------------- lib/services/api_service.dart | 38 ++-- lib/widgets/end_drawer.dart | 122 ++++++++++ lib/widgets/filter_bar.dart | 62 ++++++ lib/widgets/media_tile.dart | 46 ++++ pubspec.yaml | 2 +- 8 files changed, 373 insertions(+), 280 deletions(-) create mode 100644 lib/widgets/end_drawer.dart create mode 100644 lib/widgets/filter_bar.dart create mode 100644 lib/widgets/media_tile.dart diff --git a/lib/providers/media_provider.dart b/lib/providers/media_provider.dart index 4dc0242..102a798 100644 --- a/lib/providers/media_provider.dart +++ b/lib/providers/media_provider.dart @@ -115,6 +115,17 @@ class MediaNotifier extends StateNotifier { } } + List mergeMediaItems( + List current, + List incoming, + ) { + final existingIds = current.map((item) => item.id).toSet(); + final newItems = incoming + .where((item) => !existingIds.contains(item.id)) + .toList(); + return [...current, ...newItems]; + } + Future loadMedia({int? id}) async { if (state.isLoading) return; state = state.replace(isLoading: true); @@ -128,8 +139,11 @@ class MediaNotifier extends StateNotifier { random: state.random, tag: state.tag, ); + if (newMedia.isNotEmpty) { - addMediaItems(newMedia); + state = state.replace( + mediaItems: mergeMediaItems(state.mediaItems, newMedia), + ); } } catch (e) { print('Fehler beim Laden der Medien: $e'); diff --git a/lib/screens/detailview_screen.dart b/lib/screens/detailview_screen.dart index a6d7b16..bf53fda 100644 --- a/lib/screens/detailview_screen.dart +++ b/lib/screens/detailview_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:f0ckapp/screens/fullscreen_screen.dart'; +import 'package:f0ckapp/widgets/end_drawer.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -72,7 +73,9 @@ class _DetailViewState extends ConsumerState { Future _downloadMedia() async { final MediaState mediaState = ref.read(mediaProvider); final MediaItem currentItem = mediaState.mediaItems[_currentIndex]; - final File file = await DefaultCacheManager().getSingleFile(currentItem.mediaUrl); + final File file = await DefaultCacheManager().getSingleFile( + currentItem.mediaUrl, + ); final MethodChannel methodChannel = const MethodChannel('MediaShit'); bool? success = await methodChannel.invokeMethod('saveFile', { @@ -81,7 +84,9 @@ class _DetailViewState extends ConsumerState { }); success == true - ? _showMsg('${currentItem.dest} wurde in Downloads/fApp neigespeichert.') + ? _showMsg( + '${currentItem.dest} wurde in Downloads/fApp neigespeichert.', + ) : _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.'); } @@ -102,7 +107,7 @@ class _DetailViewState extends ConsumerState { Future.microtask(() { ref .read(mediaProvider.notifier) - .loadMedia(id: widget.initialItemId + 50); + .loadMedia(/*id: widget.initialItemId + 50*/); }); return Scaffold( appBar: AppBar(), @@ -198,8 +203,19 @@ class _DetailViewState extends ConsumerState { ], icon: const Icon(Icons.share), ), + Builder( + builder: (context) { + return IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openEndDrawer(); + }, + ); + }, + ), ], ), + endDrawer: EndDrawer(ref: ref), body: Stack( children: [ PageTransformer( diff --git a/lib/screens/mediagrid_screen.dart b/lib/screens/mediagrid_screen.dart index 09bf1c8..bd4619b 100644 --- a/lib/screens/mediagrid_screen.dart +++ b/lib/screens/mediagrid_screen.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:f0ckapp/models/mediaitem_model.dart'; import 'package:f0ckapp/providers/media_provider.dart'; -import 'package:f0ckapp/utils/appversion_util.dart'; -import 'package:f0ckapp/providers/theme_provider.dart'; import 'package:f0ckapp/utils/customsearchdelegate_util.dart'; +import 'package:f0ckapp/widgets/media_tile.dart'; +import 'package:f0ckapp/widgets/filter_bar.dart'; +import 'package:f0ckapp/widgets/end_drawer.dart'; class MediaGrid extends ConsumerStatefulWidget { const MediaGrid({super.key}); @@ -19,16 +17,6 @@ class MediaGrid extends ConsumerStatefulWidget { class _MediaGridState extends ConsumerState { final ScrollController _scrollController = ScrollController(); - final GlobalKey _scaffoldKey = GlobalKey(); - - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - - int _calculateCrossAxisCount(BuildContext context, int defaultCount) { - return defaultCount == 0 - ? (MediaQuery.of(context).size.width / 110).clamp(3, 5).toInt() - : defaultCount; - } @override void initState() { @@ -47,8 +35,6 @@ class _MediaGridState extends ConsumerState { @override void dispose() { _scrollController.dispose(); - _usernameController.dispose(); - _passwordController.dispose(); super.dispose(); } @@ -58,197 +44,98 @@ class _MediaGridState extends ConsumerState { final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier); return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: GestureDetector( - child: Row( - spacing: 10, - children: [ - Image.asset( - 'assets/images/f0ck_small.webp', - fit: BoxFit.fitHeight, + body: RefreshIndicator( + onRefresh: () async { + mediaNotifier.setTag(null); + _scrollController.jumpTo(0); + await mediaNotifier.loadMedia(); + }, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + floating: true, + snap: true, + title: GestureDetector( + onTap: () { + mediaNotifier.setTag(null); + _scrollController.jumpTo(0); + }, + child: Row( + children: [ + Image.asset( + 'assets/images/f0ck_small.webp', + fit: BoxFit.fitHeight, + ), + const SizedBox(width: 10), + const Text('fApp', style: TextStyle(fontSize: 24)), + ], + ), ), - Text('fApp', style: TextStyle(fontSize: 24)), - ], - ), - onTap: () { - mediaNotifier.setTag(null); - _scrollController.jumpTo(0); - }, - ), - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () async { - await showSearch( - context: context, - delegate: CustomSearchDelegate(), - ); - }, - ), - IconButton( - icon: Icon( - mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle, + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () async { + await showSearch( + context: context, + delegate: CustomSearchDelegate(), + ); + }, + ), + IconButton( + icon: Icon( + mediaState.random + ? Icons.shuffle_on_outlined + : Icons.shuffle, + ), + onPressed: () { + mediaNotifier.toggleRandom(); + _scrollController.jumpTo(0); + }, + ), + Builder( + builder: (context) { + return IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openEndDrawer(); + }, + ); + }, + ), + ], ), - onPressed: () { - mediaNotifier.toggleRandom(); - _scrollController.jumpTo(0); - }, - ), - IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - _scaffoldKey.currentState?.openEndDrawer(); - }, - ), - ], - ), - bottomNavigationBar: BottomAppBar( - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('type: '), - DropdownButton( - value: mediaTypes[mediaState.typeIndex], - isDense: true, - items: mediaTypes.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - mediaNotifier.setType(newValue); - _scrollController.jumpTo(0); - } - }, - ), - const Text('mode: '), - DropdownButton( - value: mediaModes[mediaState.modeIndex], - isDense: true, - items: mediaModes.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - mediaNotifier.setMode(mediaModes.indexOf(newValue)); - _scrollController.jumpTo(0); - } - }, + SliverPadding( + padding: const EdgeInsets.all(5.0), + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index >= mediaState.mediaItems.length) { + return const Center(child: CircularProgressIndicator()); + } + return MediaTile(item: mediaState.mediaItems[index]); + }, + childCount: + mediaState.mediaItems.length + + (mediaState.isLoading ? 1 : 0), + ), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + childAspectRatio: 1, + ), + ), ), ], ), ), - endDrawer: Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/menu.webp'), - fit: BoxFit.cover, - alignment: Alignment.topCenter, - ), - ), - child: null, - ), - /*ExpansionTile( - title: const Text('Login'), - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - readOnly: true, - controller: _usernameController, - decoration: const InputDecoration( - labelText: 'Benutzername', - ), - ), - const SizedBox(height: 10), - TextField( - readOnly: true, - controller: _passwordController, - obscureText: true, - decoration: const InputDecoration( - labelText: 'Passwort', - ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("noch nicht implementiert lol"), - ), - final success = await login( - _usernameController.text, - _passwordController.text, - ); - - if (success) { - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Login fehlgeschlagen!")), - ); - } - ); - }, - child: const Text('Login'), - ), - ], - ), - ), - ], - ),*/ - ExpansionTile( - title: const Text('Theme'), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: themeMap.entries.map((entry) { - final String themeName = entry.key; - final ThemeData themeData = entry.value; - final ThemeData currentTheme = ref.watch(themeNotifierProvider); - final bool isSelected = currentTheme == themeData; - return ListTile( - title: Text(themeName), - selected: isSelected, - selectedTileColor: Colors.blue.withValues(alpha: 0.2), - onTap: () async { - await ref - .read(themeNotifierProvider.notifier) - .updateTheme(themeName); - }, - ); - }).toList(), - ), - ), - ], - ), - ListTile( - title: Text('v${AppVersion.version}'), - onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('jooong lass das, hier ist nichts'), - ), - ); - }, - ), - ], - ), + bottomNavigationBar: FilterBar( + mediaNotifier: mediaNotifier, + mediaState: mediaState, + scrollController: _scrollController, ), + endDrawer: EndDrawer(ref: ref), persistentFooterButtons: mediaState.tag != null ? [ Center( @@ -262,62 +149,6 @@ class _MediaGridState extends ConsumerState { ), ] : null, - body: RefreshIndicator( - onRefresh: () async { - mediaNotifier.resetMedia(); - _scrollController.jumpTo(0); - }, - child: GridView.builder( - key: const PageStorageKey('mediaGrid'), - controller: _scrollController, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _calculateCrossAxisCount( - context, - mediaState.crossAxisCount, - ), - crossAxisSpacing: 5.0, - mainAxisSpacing: 5.0, - ), - itemCount: - mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0), - itemBuilder: (BuildContext context, int index) { - if (index >= mediaState.mediaItems.length) { - return const Center(child: CircularProgressIndicator()); - } - final MediaItem item = mediaState.mediaItems[index]; - - return InkWell( - onTap: () async { - context.push('/${item.id}', extra: true); - }, - child: Stack( - fit: StackFit.expand, - children: [ - CachedNetworkImage( - imageUrl: item.thumbnailUrl, - fit: BoxFit.cover, - placeholder: (context, url) => const SizedBox.shrink(), - errorWidget: (context, url, error) => - const Icon(Icons.error), - ), - Align( - alignment: Alignment.bottomRight, - child: Icon( - Icons.square, - color: switch (item.mode) { - 1 => Colors.green, - 2 => Colors.red, - _ => Colors.yellow, - }, - size: 15.0, - ), - ), - ], - ), - ); - }, - ), - ), ); } } diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 4287644..6fa24c1 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -13,16 +13,16 @@ final FlutterSecureStorage storage = const FlutterSecureStorage( Future> fetchMedia({ int? older, - String? type, - int? mode, - bool? random, + String type = 'image', + int mode = 0, + bool random = false, String? tag, }) async { final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace( queryParameters: { - 'type': type ?? 'image', - 'mode': (mode ?? 0).toString(), - 'random': (random! ? 1 : 0).toString(), + 'type': type, + 'mode': mode.toString(), + 'random': (random ? 1 : 0).toString(), if (tag != null) 'tag': tag, if (older != null) 'older': older.toString(), }, @@ -61,14 +61,14 @@ Future> fetchSuggestions(String query) async { if (response.statusCode == 200) { final dynamic decoded = jsonDecode(response.body); - if (decoded is List) { - return decoded + final suggestions = decoded .map((item) => Suggestion.fromJson(item as Map)) - .toList() - ..sort((a, b) => b.score.compareTo(a.score)); + .toList(); + suggestions.sort((a, b) => b.score.compareTo(a.score)); + return suggestions; } else { - throw Exception('Unerwartetes Format: Erwartet wurde eine Liste.'); + throw Exception('Unerwartetes Format: Es wurde eine Liste erwartet.'); } } else if (response.statusCode == 400) { final dynamic error = jsonDecode(response.body); @@ -84,7 +84,7 @@ Future> fetchSuggestions(String query) async { } on TimeoutException { throw Exception('Anfrage an die API hat zu lange gedauert.'); } catch (e) { - throw Exception('Fehler beim Verarbeiten der Anfrage: $e'); + throw Exception('Fehler bei der Verarbeitung der Anfrage: $e'); } } @@ -98,12 +98,14 @@ Future login(String username, String password) async { if (response.statusCode == 200) { final dynamic data = jsonDecode(response.body); - final dynamic token = data['token']; - - await storage.write(key: "token", value: token); - - return true; + final token = data['token']; + if (token != null) { + await storage.write(key: "token", value: token); + return true; + } else { + throw Exception('Token nicht im Response enthalten.'); + } } else { - return false; + throw Exception('Login fehlgeschlagen: ${response.statusCode}'); } } diff --git a/lib/widgets/end_drawer.dart b/lib/widgets/end_drawer.dart new file mode 100644 index 0000000..47e9194 --- /dev/null +++ b/lib/widgets/end_drawer.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:f0ckapp/providers/theme_provider.dart'; +import 'package:f0ckapp/utils/appversion_util.dart'; + +class EndDrawer extends StatelessWidget { + final WidgetRef ref; + + const EndDrawer({super.key, required this.ref}); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/menu.webp'), + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), + child: null, + ), + /*ExpansionTile( + title: const Text('Login'), + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + readOnly: true, + controller: _usernameController, + decoration: const InputDecoration( + labelText: 'Benutzername', + ), + ), + const SizedBox(height: 10), + TextField( + readOnly: true, + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Passwort', + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("noch nicht implementiert lol"), + ), + final success = await login( + _usernameController.text, + _passwordController.text, + ); + + if (success) { + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Login fehlgeschlagen!")), + ); + } + ); + }, + child: const Text('Login'), + ), + ], + ), + ), + ], + ),*/ + ExpansionTile( + title: const Text('Theme'), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: themeMap.entries.map((entry) { + final String themeName = entry.key; + final ThemeData themeData = entry.value; + final ThemeData currentTheme = ref.watch( + themeNotifierProvider, + ); + final bool isSelected = currentTheme == themeData; + return ListTile( + title: Text(themeName), + selected: isSelected, + selectedTileColor: Colors.blue.withValues(alpha: 0.2), + onTap: () async { + await ref + .read(themeNotifierProvider.notifier) + .updateTheme(themeName); + }, + ); + }).toList(), + ), + ), + ], + ), + ListTile( + title: Text('v${AppVersion.version}'), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('jooong lass das, hier ist nichts'), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/filter_bar.dart b/lib/widgets/filter_bar.dart new file mode 100644 index 0000000..80d7124 --- /dev/null +++ b/lib/widgets/filter_bar.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:f0ckapp/providers/media_provider.dart'; + +class FilterBar extends StatelessWidget { + final MediaState mediaState; + final MediaNotifier mediaNotifier; + final ScrollController scrollController; + + const FilterBar({ + super.key, + required this.mediaState, + required this.mediaNotifier, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return BottomAppBar( + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('type: '), + DropdownButton( + value: mediaTypes[mediaState.typeIndex], + isDense: true, + items: mediaTypes.map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + mediaNotifier.setType(newValue); + scrollController.jumpTo(0); + } + }, + ), + const Text('mode: '), + DropdownButton( + value: mediaModes[mediaState.modeIndex], + isDense: true, + items: mediaModes.map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + mediaNotifier.setMode(mediaModes.indexOf(newValue)); + scrollController.jumpTo(0); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/media_tile.dart b/lib/widgets/media_tile.dart new file mode 100644 index 0000000..0097783 --- /dev/null +++ b/lib/widgets/media_tile.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:f0ckapp/models/mediaitem_model.dart'; + +class MediaTile extends StatelessWidget { + final MediaItem item; + + const MediaTile({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + context.push('/${item.id}', extra: true); + }, + child: Stack( + fit: StackFit.expand, + children: [ + Hero( + tag: 'media-${item.id}', + child: CachedNetworkImage( + imageUrl: item.thumbnailUrl, + fit: BoxFit.cover, + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Icon( + Icons.square, + color: switch (item.mode) { + 1 => Colors.green, + 2 => Colors.red, + _ => Colors.yellow, + }, + size: 15.0, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 6c191eb..1d6e1d4 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.1.19+49 +version: 1.1.20+50 environment: sdk: ^3.9.0-100.2.beta