From 0d9ed1e6c162b5b0ed12b6ec5aac5b17583da271 Mon Sep 17 00:00:00 2001 From: Flummi Date: Thu, 5 Jun 2025 19:53:25 +0200 Subject: [PATCH] v1.0.26+26 --- assets/images/menu.webp | Bin 0 -> 16808 bytes lib/main.dart | 21 +- lib/providers/MediaProvider.dart | 88 +++++++ lib/providers/ThemeProvider.dart | 132 ++++++++++ lib/screens/DetailView.dart | 200 +++++---------- lib/screens/MediaGrid.dart | 426 +++++++++++-------------------- lib/services/Api.dart | 4 +- pubspec.lock | 16 ++ pubspec.yaml | 3 +- 9 files changed, 465 insertions(+), 425 deletions(-) create mode 100644 assets/images/menu.webp create mode 100644 lib/providers/MediaProvider.dart create mode 100644 lib/providers/ThemeProvider.dart diff --git a/assets/images/menu.webp b/assets/images/menu.webp new file mode 100644 index 0000000000000000000000000000000000000000..ff6b76add7722e34c9c50ee2cf130aa6d9c6facb GIT binary patch literal 16808 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 DiwacC literal 0 HcmV?d00001 diff --git a/lib/main.dart b/lib/main.dart index 89ff36b..f73b914 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:f0ckapp/providers/MediaProvider.dart'; +import 'package:f0ckapp/providers/ThemeProvider.dart'; import 'package:f0ckapp/screens/MediaGrid.dart'; +import 'package:provider/provider.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - runApp(const F0ckApp()); + + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => ThemeProvider()), + ChangeNotifierProvider(create: (context) => MediaProvider()) + ], + child: F0ckApp() + ) + ); } class F0ckApp extends StatelessWidget { @@ -14,11 +27,11 @@ class F0ckApp extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return MaterialApp( debugShowCheckedModeBanner: false, - theme: ThemeData( - scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23), - ), + theme: themeProvider.themeData, home: Scaffold( body: MediaGrid(), ), diff --git a/lib/providers/MediaProvider.dart b/lib/providers/MediaProvider.dart new file mode 100644 index 0000000..0a7acf7 --- /dev/null +++ b/lib/providers/MediaProvider.dart @@ -0,0 +1,88 @@ +import 'package:f0ckapp/services/Api.dart'; +import 'package:flutter/material.dart'; +import 'package:f0ckapp/models/MediaItem.dart'; + +class MediaProvider extends ChangeNotifier { + int _typeid = 0; + int _mode = 0; + bool _random = false; + String? _tag; + int _crossAxisCount = 0; + final List _mediaItems = []; + bool _isLoading = false; + + List types = ["alles", "image", "video", "audio"]; + List modes = ["sfw", "nsfw", "untagged", "all"]; + + String get type => types[_typeid]; + int get typeid => _typeid; + int get mode => _mode; + bool get random => _random; + String? get tag => _tag; + int get crossAxisCount => _crossAxisCount; + List get mediaItems => _mediaItems; + bool get isLoading => _isLoading; + + void setType(String type) { + _typeid = types.indexOf(type); + loadMedia(reload: true); + } + + void setMode(int mode) { + _mode = mode; + loadMedia(reload: true); + } + + void toggleRandom() { + _random = !_random; + loadMedia(reload: true); + } + + void setTag(String? tag) { + _tag = tag; + loadMedia(reload: true); + } + + void setCrossAxisCount(int crossAxisCount) { + _crossAxisCount = crossAxisCount; + notifyListeners(); + } + + void setMediaItems(List mediaItems) { + _mediaItems.clear(); + addMediaItems(mediaItems); + notifyListeners(); + } + + void addMediaItems(List newItems) { + _mediaItems.addAll(newItems); + notifyListeners(); + } + + Future loadMedia({bool reload = false}) async { + if (_isLoading) return; + _isLoading = true; + notifyListeners(); + + try { + final newMedia = await fetchMedia( + older: reload + ? null + : _mediaItems.isNotEmpty + ? _mediaItems.last.id + : null, + type: type, + mode: mode, + random: random, + tag: tag, + ); + + reload ? setMediaItems(newMedia) : addMediaItems(newMedia); + } catch (e) { + debugPrint('Fehler beim Laden der Medien: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/ThemeProvider.dart b/lib/providers/ThemeProvider.dart new file mode 100644 index 0000000..2576fb4 --- /dev/null +++ b/lib/providers/ThemeProvider.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; + +final ThemeData f0ckTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Color(0xFF9FFF00), + scaffoldBackgroundColor: Color(0xFF000000), + colorScheme: ColorScheme.dark( + primary: Color(0xFF9FFF00), + secondary: Color(0xFF262626), + surface: Color(0xFF232323), + onPrimary: Color(0xFF000000), + onSecondary: Color(0xFFFFFFFF), + onSurface: Color(0xFFFFFFFF), + ), + appBarTheme: AppBarTheme( + backgroundColor: Color(0xFF2B2B2B), + foregroundColor: Color(0xFF9FFF00), + elevation: 2, + ), + textTheme: TextTheme( + bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFFFFFFFF)), + bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFFFFFFFF)), + ), + buttonTheme: ButtonThemeData( + buttonColor: Color(0xFF9FFF00), + textTheme: ButtonTextTheme.primary, + ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)), + trackColor: WidgetStateProperty.all(Color(0xFF424242)), + ), +); + +final ThemeData paperTheme = ThemeData( + brightness: Brightness.light, + primaryColor: Color(0xFF000000), + scaffoldBackgroundColor: Color(0xFFFFFFFF), + colorScheme: ColorScheme.light( + primary: Color(0xFF000000), + secondary: Color(0xFF262626), + surface: Color(0xFFFFFFFF), + onPrimary: Color(0xFFFFFFFF), + onSecondary: Color(0xFFFFFFFF), + onSurface: Color(0xFF000000), + ), + appBarTheme: AppBarTheme( + backgroundColor: Color(0xFFFFFFFF), + foregroundColor: Color(0xFF000000), + elevation: 0, + ), + textTheme: TextTheme( + bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFF000000)), + bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFF000000)), + ), + buttonTheme: ButtonThemeData( + buttonColor: Color(0xFF000000), + textTheme: ButtonTextTheme.primary, + ), +); + +final ThemeData f0ck95Theme = ThemeData( + brightness: Brightness.light, + primaryColor: Color(0xFFC0C0C0), + scaffoldBackgroundColor: Color(0xFF008080), + colorScheme: ColorScheme.light( + primary: Color(0xFFC0C0C0), + secondary: Color(0xFF808080), + surface: Color(0xFFC0C0C0), + onPrimary: Color(0xFF000000), + onSecondary: Color(0xFFFFFFFF), + ), + appBarTheme: AppBarTheme( + backgroundColor: Color(0xFFC0C0C0), + foregroundColor: Color(0xFF000000), + elevation: 2, + ), + textTheme: TextTheme( + bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFF000000)), + bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFF000000)), + ), + buttonTheme: ButtonThemeData( + buttonColor: Color(0xFF000000), + textTheme: ButtonTextTheme.primary, + ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)), + trackColor: WidgetStateProperty.all(Color(0xFF424242)), + ), +); + +final ThemeData f0ck95dTheme = ThemeData( + brightness: Brightness.dark, + primaryColor: Color(0xFFFFFFFF), + scaffoldBackgroundColor: Color(0xFF0E0F0F), + colorScheme: ColorScheme.dark( + primary: Color(0xFFFFFFFF), + secondary: Color(0xFFC0C0C0), + surface: Color(0xFF333131), + onPrimary: Color(0xFF000000), + onSecondary: Color(0xFFFFFFFF), + ), + appBarTheme: AppBarTheme( + backgroundColor: Color(0xFF0B0A0A), + foregroundColor: Color(0xFFFFFFFF), + elevation: 2, + ), + textTheme: TextTheme( + bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFFFFFFFF)), + bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFFFFFFFF)), + ), + buttonTheme: ButtonThemeData( + buttonColor: Color(0xFFFFFFFF), + textTheme: ButtonTextTheme.primary, + ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)), + trackColor: WidgetStateProperty.all(Color(0xFF424242)), + ), +); + + + +class ThemeProvider extends ChangeNotifier { + ThemeData _themeData = f0ck95dTheme; + + ThemeData get themeData => _themeData; + + /*void toggleTheme() { + _themeData = _themeData == lightTheme ? darkTheme : lightTheme; + notifyListeners(); + }*/ +} diff --git a/lib/screens/DetailView.dart b/lib/screens/DetailView.dart index 25ef09c..06872f1 100644 --- a/lib/screens/DetailView.dart +++ b/lib/screens/DetailView.dart @@ -1,29 +1,18 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:provider/provider.dart'; import 'package:f0ckapp/models/MediaItem.dart'; import 'package:f0ckapp/services/Api.dart'; import 'package:f0ckapp/widgets/VideoWidget.dart'; import 'package:f0ckapp/utils/SmartRefreshIndicator.dart'; import 'package:f0ckapp/utils/PageTransformer.dart'; +import 'package:f0ckapp/providers/MediaProvider.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; class DetailView extends StatefulWidget { final int initialItemId; - final List mediaItems; - final String type; - final int mode; - final bool random; - final String? tagname; - const DetailView({ - super.key, - required this.initialItemId, - required this.mediaItems, - required this.type, - required this.mode, - required this.random, - required this.tagname, - }); + const DetailView({super.key, required this.initialItemId}); @override State createState() => _DetailViewState(); @@ -31,57 +20,34 @@ class DetailView extends StatefulWidget { class _DetailViewState extends State { late PageController _pageController; - late List mediaItems; - String? _tagname; - int currentItemId = 0; bool isLoading = false; @override void initState() { super.initState(); - mediaItems = widget.mediaItems; - _tagname = widget.tagname; - final initialIndex = mediaItems.indexWhere( + final provider = Provider.of(context, listen: false); + + final initialIndex = provider.mediaItems.indexWhere( (item) => item.id == widget.initialItemId, ); _pageController = PageController(initialPage: initialIndex); - currentItemId = mediaItems[initialIndex].id; - - int? lastLoadedIndex; - _pageController.addListener(() async { - final newIndex = _pageController.page?.round(); - if (newIndex != null && - newIndex < mediaItems.length && - newIndex != lastLoadedIndex) { - setState(() => currentItemId = mediaItems[newIndex].id); - lastLoadedIndex = newIndex; - - _preloadAdjacentMedia(newIndex); - } - - if (_pageController.position.pixels >= - _pageController.position.maxScrollExtent - 100) { - _loadMoreMedia(); - } - }); _preloadAdjacentMedia(initialIndex); } void _preloadAdjacentMedia(int index) async { - if (index + 1 < mediaItems.length) { - final nextUrl = mediaItems[index + 1].mediaUrl; + final provider = Provider.of(context, listen: false); + if (index + 1 < provider.mediaItems.length) { + final nextUrl = provider.mediaItems[index + 1].mediaUrl; if (await DefaultCacheManager().getFileFromCache(nextUrl) == null) { await DefaultCacheManager().downloadFile(nextUrl); - print('preload ${mediaItems[index + 1].id}'); } } if (index - 1 >= 0) { - final prevUrl = mediaItems[index - 1].mediaUrl; + final prevUrl = provider.mediaItems[index - 1].mediaUrl; if (await DefaultCacheManager().getFileFromCache(prevUrl) == null) { await DefaultCacheManager().downloadFile(prevUrl); - print('preload ${mediaItems[index - 1].id}'); } } } @@ -90,116 +56,84 @@ class _DetailViewState extends State { if (isLoading) return; setState(() => isLoading = true); + final provider = Provider.of(context, listen: false); + try { final newMedia = await fetchMedia( - older: mediaItems.last.id.toString(), - type: widget.type, - mode: widget.mode, - random: widget.random, - tag: _tagname, + older: provider.mediaItems.last.id, + type: provider.type, + mode: provider.mode, + random: provider.random, + tag: provider.tag, ); if (mounted && newMedia.isNotEmpty) { - setState(() { - mediaItems.addAll(newMedia); - isLoading = false; - }); + setState(() => provider.mediaItems.addAll(newMedia)); } } catch (e) { - _showError("Ein unerwarteter Fehler ist aufgetreten: $e"); - } - } - - Future _refreshMediaItem() async { - try { - final updatedItem = await fetchMediaDetail(currentItemId); - if (mounted) { - final index = mediaItems.indexWhere((item) => item.id == currentItemId); - if (index != -1) { - setState(() => mediaItems[index] = updatedItem); - } - } - } catch (e) { - _showError("Fehler beim Aktualisieren des Items: $e"); + _showError("Fehler beim Laden der Medien: $e"); + } finally { + setState(() => isLoading = false); } } void _showError(String message) { if (!mounted) return; - - final messenger = ScaffoldMessenger.of(context); - messenger.hideCurrentSnackBar(); - messenger.showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { + final provider = Provider.of(context); + return Scaffold( - backgroundColor: const Color(0xFF171717), appBar: AppBar( - backgroundColor: const Color(0xFF2B2B2B), - foregroundColor: Colors.white, - title: Text('f0ck #$currentItemId (${widget.type})'), centerTitle: true, + title: Text('f0ck #${widget.initialItemId} (${provider.type})'), ), body: Stack( children: [ PageTransformer( controller: _pageController, - pages: mediaItems.map((item) { - final isActive = item.id == currentItemId; - return Scaffold( - body: SafeArea( - child: SmartRefreshIndicator( - onRefresh: _refreshMediaItem, - child: _buildMediaItem(item, isActive), - ), + pages: provider.mediaItems.map((item) { + return SafeArea( + child: SmartRefreshIndicator( + onRefresh: _loadMoreMedia, + child: _buildMediaItem(item), ), ); }).toList(), ), - if (_tagname != null) - Positioned( - bottom: 60, - left: MediaQuery.of(context).size.width * 0.2, - right: MediaQuery.of(context).size.width * 0.2, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 20, - ), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Tag: $_tagname', - style: const TextStyle(color: Colors.white), - ), - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () { - setState(() { - _tagname = '___empty___'; - Navigator.pop(context, _tagname); - }); - }, - ), - ], - ), - ), - ), ], ), + persistentFooterButtons: provider.tag != null + ? [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('tag: '), + InputChip( + label: Text(provider.tag!), + backgroundColor: const Color(0xFF090909), + labelStyle: const TextStyle(color: Colors.white), + onDeleted: () { + provider.setTag(null); + Navigator.pop(context); + }, + ), + ], + ), + ] + : null, ); } - Widget _buildMediaItem(MediaItem item, bool isActive) { + Widget _buildMediaItem(MediaItem item) { + final provider = Provider.of(context); + return SingleChildScrollView( child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ if (item.mime.startsWith('image')) CachedNetworkImage( @@ -209,7 +143,7 @@ class _DetailViewState extends State { errorWidget: (context, url, error) => Icon(Icons.error), ) else - VideoWidget(details: item, isActive: isActive), + VideoWidget(details: item, isActive: true), const SizedBox(height: 20), Text( item.mime, @@ -221,26 +155,20 @@ class _DetailViewState extends State { spacing: 5.0, children: item.tags.map((tag) { return ActionChip( - label: Text( - tag.tag, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), + onPressed: () { + if (tag.tag == 'sfw' || tag.tag == 'nsfw') return; + setState(() { + provider.setTag(tag.tag); + Navigator.pop(context); + }); + }, + label: Text(tag.tag), backgroundColor: switch (tag.id) { 1 => Colors.green, 2 => Colors.red, _ => const Color(0xFF090909), }, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - onPressed: () async { - if (tag.tag == 'sfw' || tag.tag == 'nsfw') return; - setState(() => Navigator.pop(context, tag.tag)); - }, + labelStyle: const TextStyle(color: Colors.white), ); }).toList(), ), diff --git a/lib/screens/MediaGrid.dart b/lib/screens/MediaGrid.dart index 1e67478..a259941 100644 --- a/lib/screens/MediaGrid.dart +++ b/lib/screens/MediaGrid.dart @@ -1,9 +1,9 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:f0ckapp/services/Api.dart'; -import 'package:f0ckapp/models/MediaItem.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:provider/provider.dart'; import 'package:f0ckapp/screens/DetailView.dart'; -import 'dart:async'; +import 'package:f0ckapp/services/Api.dart'; +import 'package:f0ckapp/providers/MediaProvider.dart'; class MediaGrid extends StatefulWidget { const MediaGrid({super.key}); @@ -14,256 +14,175 @@ class MediaGrid extends StatefulWidget { class _MediaGridState extends State { final ScrollController _scrollController = ScrollController(); - final String _version = '1.0.25+25'; - List mediaItems = []; - bool isLoading = false; - Timer? _debounceTimer; - Completer? _navigationCompleter; - int _crossAxisCount = 0; - String _selectedType = 'alles'; - int _selectedMode = 0; - bool _random = false; - final List _modes = ["sfw", "nsfw", "untagged", "all"]; - String? _selectedTag; @override void initState() { super.initState(); - _loadMedia(); + + final provider = Provider.of(context, listen: false); + + Future.microtask(() { + provider.loadMedia(); + }); + _scrollController.addListener(() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) { - _debounceLoadMedia(); + provider.loadMedia(); } }); } - void _debounceLoadMedia() { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 500), _loadMedia); - } - - int _calculateCrossAxisCount(BuildContext context) { - if (_crossAxisCount != 0) { - return _crossAxisCount; - } - - double screenWidth = MediaQuery.of(context).size.width; - int columnCount = (screenWidth / 110).clamp(3, 5).toInt(); - - return columnCount; - } - - Future _loadMedia() async { - if (isLoading) return; - setState(() => isLoading = true); - - try { - final newMedia = await fetchMedia( - older: mediaItems.isNotEmpty ? mediaItems.last.id.toString() : null, - type: _selectedType, - mode: _selectedMode, - random: _random, - tag: _selectedTag, - ); - if (mounted) { - setState(() => mediaItems.addAll(newMedia)); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Fehler beim Laden der Medien: $e')), - ); - } - } finally { - if (mounted) setState(() => isLoading = false); - } - } - - Future _refreshMedia() async { - setState(() => isLoading = true); - try { - final freshMedia = await fetchMedia( - older: null, - type: _selectedType, - mode: _selectedMode, - random: _random, - tag: _selectedTag, - ); - if (mounted) { - setState(() { - mediaItems.clear(); - mediaItems.addAll(freshMedia); - }); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Fehler beim Aktualisieren: $e')), - ); - } - } finally { - if (mounted) setState(() => isLoading = false); - } - } - - Future _navigateToDetail(MediaItem item) async { - if (_navigationCompleter?.isCompleted == false) return; - - _navigationCompleter = Completer(); - try { - if (mounted) { - final String? newTag = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DetailView( - initialItemId: item.id, - mediaItems: mediaItems, - type: _selectedType, - mode: _selectedMode, - random: _random, - tagname: _selectedTag, - ), - ), - ); - - if (newTag != null) { - setState(() { - if (newTag == '___empty___') { - _selectedTag = null; - } - else { - _selectedTag = newTag; - } - _refreshMedia(); - }); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Fehler beim Laden der Details: $e')), - ); - } - } finally { - _navigationCompleter?.complete(); - } - } - @override Widget build(BuildContext context) { + final provider = Provider.of(context); + final GlobalKey scaffoldKey = GlobalKey(); + return Scaffold( + key: scaffoldKey, appBar: AppBar( - centerTitle: true, - backgroundColor: const Color.fromARGB(255, 43, 43, 43), - foregroundColor: const Color.fromARGB(255, 255, 255, 255), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + //centerTitle: true, + title: Text('f0ck v1.0.26+26'), + actions: [ + DropdownButton( + // mode + value: provider.modes[provider.mode], + isDense: true, + icon: SizedBox.shrink(), + items: provider.modes.map((String value) { + return DropdownMenuItem(value: value, child: Text(value)); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + provider.setMode(provider.modes.indexOf(newValue)); + } + }, + ), + IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + scaffoldKey.currentState?.openEndDrawer(); + }, + ), + ], + ), + endDrawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, children: [ - Text('f0ck v$_version'), - Checkbox( - value: _random, - onChanged: (bool? value) { - setState(() { - _random = !_random; - _refreshMedia(); - }); + DrawerHeader( + padding: EdgeInsets.all(0), + child: Image.asset('assets/images/menu.webp', fit: BoxFit.cover), + ), + ListTile( + title: Text( + 'All', + style: TextStyle( + fontWeight: provider.type == 'alles' + ? FontWeight.bold + : FontWeight.normal, + color: provider.type == 'alles' ? Colors.blue : Colors.white, + ), + ), + onTap: () { + provider.setType('all'); + }, + ), + ListTile( + title: Text( + 'Images', + style: TextStyle( + fontWeight: provider.type == 'image' + ? FontWeight.bold + : FontWeight.normal, + color: provider.type == 'image' ? Colors.blue : Colors.white, + ), + ), + onTap: () { + provider.setType('image'); + }, + ), + ListTile( + title: Text( + 'Videos', + style: TextStyle( + fontWeight: provider.type == 'video' + ? FontWeight.bold + : FontWeight.normal, + color: provider.type == 'video' ? Colors.blue : Colors.white, + ), + ), + onTap: () { + provider.setType('video'); + }, + ), + ListTile( + title: Text( + 'Audio', + style: TextStyle( + fontWeight: provider.type == 'audio' + ? FontWeight.bold + : FontWeight.normal, + color: provider.type == 'audio' ? Colors.blue : Colors.white, + ), + ), + onTap: () { + provider.setType('audio'); }, ), ], ), ), - bottomNavigationBar: BottomAppBar( - color: const Color.fromARGB(255, 43, 43, 43), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - DropdownButton( - value: _selectedType, - dropdownColor: const Color.fromARGB(255, 43, 43, 43), - iconEnabledColor: Colors.white, - items: ["alles", "image", "video", "audio"].map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value, style: TextStyle(color: Colors.white)), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedType = newValue; - _refreshMedia(); - }); - } - }, + persistentFooterButtons: provider.tag != null + ? [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('tag: '), + InputChip( + label: Text(provider.tag!), + backgroundColor: const Color(0xFF090909), + labelStyle: const TextStyle(color: Colors.white), + onDeleted: () { + provider.setTag(null); + }, + ), + ], ), - DropdownButton( - value: _modes[_selectedMode], - dropdownColor: const Color.fromARGB(255, 43, 43, 43), - iconEnabledColor: Colors.white, - items: _modes.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value, style: TextStyle(color: Colors.white)), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedMode = _modes.indexOf(newValue); - _refreshMedia(); - }); - } - }, - ), - DropdownButton( - value: _crossAxisCount, - dropdownColor: const Color.fromARGB(255, 43, 43, 43), - iconEnabledColor: Colors.white, - items: [0, 3, 4].map((int value) { - return DropdownMenuItem( - value: value, - child: Text( - value == 0 ? 'auto' : '$value Spalten', - style: TextStyle(color: Colors.white), - ), - ); - }).toList(), - onChanged: (int? newValue) { - if (newValue != null) { - setState(() { - _crossAxisCount = newValue; - }); - } - }, - ), - ], - ), - ), - ), - body: Stack( - children: [ - RefreshIndicator( - onRefresh: _refreshMedia, - child: GridView.builder( + ] + : null, + body: RefreshIndicator( + onRefresh: () async { + await provider.loadMedia(reload: true); + }, + child: Consumer( + builder: (context, mediaProvider, child) { + return GridView.builder( controller: _scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _calculateCrossAxisCount(context), + crossAxisCount: mediaProvider.crossAxisCount == 0 + ? (MediaQuery.of(context).size.width / 110) + .clamp(3, 5) + .toInt() + : mediaProvider.crossAxisCount, crossAxisSpacing: 5.0, mainAxisSpacing: 5.0, ), - itemCount: mediaItems.length + (isLoading ? 1 : 0), + itemCount: + provider.mediaItems.length + (provider.isLoading ? 1 : 0), itemBuilder: (context, index) { - if (index >= mediaItems.length) { + if (index >= provider.mediaItems.length) { return const Center(child: CircularProgressIndicator()); } - final item = mediaItems[index]; + final item = provider.mediaItems[index]; return InkWell( - onTap: () => _navigateToDetail(item), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DetailView(initialItemId: item.id), + ), + ), child: Stack( fit: StackFit.expand, children: [ @@ -289,67 +208,10 @@ class _MediaGridState extends State { ), ); }, - ), - ), - if (_selectedTag != null) - Positioned( - bottom: 20, - left: MediaQuery.of(context).size.width * 0.2, - right: MediaQuery.of(context).size.width * 0.2, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 20, - ), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Tag: $_selectedTag', - style: const TextStyle(color: Colors.white), - ), - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () { - setState(() { - _selectedTag = null; - _refreshMedia(); - }); - _scrollController.animateTo( - 0.0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeOut, - ); - }, - ), - ], - ), - ), - ), - ], + ); + }, + ), ), - /*floatingActionButton: FloatingActionButton( - backgroundColor: Colors.black.withValues(alpha: 0.8), - child: const Icon(Icons.arrow_upward, color: Colors.white), - onPressed: () { - _scrollController.animateTo( - 0.0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeOut, - ); - }, - ),*/ ); } - - @override - void dispose() { - _scrollController.dispose(); - _debounceTimer?.cancel(); - super.dispose(); - } } diff --git a/lib/services/Api.dart b/lib/services/Api.dart index 9c88cfa..66c6eed 100644 --- a/lib/services/Api.dart +++ b/lib/services/Api.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart' as http; import 'package:f0ckapp/models/MediaItem.dart'; Future> fetchMedia({ - String? older, + int? older, String? type, int? mode, bool? random, @@ -16,7 +16,7 @@ Future> fetchMedia({ 'mode': (mode ?? 0).toString(), 'random': (random! ? 1 : 0).toString(), if (tag != null) 'tag': tag, - if (older != null) 'older': older, + if (older != null) 'older': older.toString(), }, ); diff --git a/pubspec.lock b/pubspec.lock index 565f08c..d3322be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -256,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" octo_image: dependency: transitive description: @@ -336,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4ff2c76..62f6432 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.0.25+25 +version: 1.0.26+26 environment: sdk: ^3.9.0-100.2.beta @@ -37,6 +37,7 @@ dependencies: cupertino_icons: ^1.0.8 cached_network_image: ^3.4.1 cached_video_player_plus: ^3.0.3 + provider: ^6.1.5 dev_dependencies: flutter_test: