This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								assets/images/menu.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/menu.webp
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 16 KiB  | 
@@ -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<ThemeProvider>(context);
 | 
			
		||||
 | 
			
		||||
    return MaterialApp(
 | 
			
		||||
      debugShowCheckedModeBanner: false,
 | 
			
		||||
      theme: ThemeData(
 | 
			
		||||
        scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23),
 | 
			
		||||
      ),
 | 
			
		||||
      theme: themeProvider.themeData,
 | 
			
		||||
      home: Scaffold(
 | 
			
		||||
        body: MediaGrid(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										88
									
								
								lib/providers/MediaProvider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								lib/providers/MediaProvider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<MediaItem> _mediaItems = [];
 | 
			
		||||
  bool _isLoading = false;
 | 
			
		||||
 | 
			
		||||
  List<String> types = ["alles", "image", "video", "audio"];
 | 
			
		||||
  List<String> 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<MediaItem> 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<MediaItem> mediaItems) {
 | 
			
		||||
    _mediaItems.clear();
 | 
			
		||||
    addMediaItems(mediaItems);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addMediaItems(List<MediaItem> newItems) {
 | 
			
		||||
    _mediaItems.addAll(newItems);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> 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();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								lib/providers/ThemeProvider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								lib/providers/ThemeProvider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
  }*/
 | 
			
		||||
}
 | 
			
		||||
@@ -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<MediaItem> 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<DetailView> {
 | 
			
		||||
  late PageController _pageController;
 | 
			
		||||
  late List<MediaItem> 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<MediaProvider>(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<MediaProvider>(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<DetailView> {
 | 
			
		||||
    if (isLoading) return;
 | 
			
		||||
    setState(() => isLoading = true);
 | 
			
		||||
 | 
			
		||||
    final provider = Provider.of<MediaProvider>(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<void> _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<MediaProvider>(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<MediaProvider>(context);
 | 
			
		||||
 | 
			
		||||
    return SingleChildScrollView(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          if (item.mime.startsWith('image'))
 | 
			
		||||
            CachedNetworkImage(
 | 
			
		||||
@@ -209,7 +143,7 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
              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<DetailView> {
 | 
			
		||||
            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(),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -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<MediaGrid> {
 | 
			
		||||
  final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  final String _version = '1.0.25+25';
 | 
			
		||||
  List<MediaItem> mediaItems = [];
 | 
			
		||||
  bool isLoading = false;
 | 
			
		||||
  Timer? _debounceTimer;
 | 
			
		||||
  Completer<void>? _navigationCompleter;
 | 
			
		||||
  int _crossAxisCount = 0;
 | 
			
		||||
  String _selectedType = 'alles';
 | 
			
		||||
  int _selectedMode = 0;
 | 
			
		||||
  bool _random = false;
 | 
			
		||||
  final List<String> _modes = ["sfw", "nsfw", "untagged", "all"];
 | 
			
		||||
  String? _selectedTag;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _loadMedia();
 | 
			
		||||
 | 
			
		||||
    final provider = Provider.of<MediaProvider>(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<void> _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<void> _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<void> _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<MediaProvider>(context);
 | 
			
		||||
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
 | 
			
		||||
 | 
			
		||||
    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<String>(
 | 
			
		||||
            // mode
 | 
			
		||||
            value: provider.modes[provider.mode],
 | 
			
		||||
            isDense: true,
 | 
			
		||||
            icon: SizedBox.shrink(),
 | 
			
		||||
            items: provider.modes.map((String value) {
 | 
			
		||||
              return DropdownMenuItem<String>(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<String>(
 | 
			
		||||
                value: _selectedType,
 | 
			
		||||
                dropdownColor: const Color.fromARGB(255, 43, 43, 43),
 | 
			
		||||
                iconEnabledColor: Colors.white,
 | 
			
		||||
                items: ["alles", "image", "video", "audio"].map((String value) {
 | 
			
		||||
                  return DropdownMenuItem<String>(
 | 
			
		||||
                    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<String>(
 | 
			
		||||
                value: _modes[_selectedMode],
 | 
			
		||||
                dropdownColor: const Color.fromARGB(255, 43, 43, 43),
 | 
			
		||||
                iconEnabledColor: Colors.white,
 | 
			
		||||
                items: _modes.map((String value) {
 | 
			
		||||
                  return DropdownMenuItem<String>(
 | 
			
		||||
                    value: value,
 | 
			
		||||
                    child: Text(value, style: TextStyle(color: Colors.white)),
 | 
			
		||||
                  );
 | 
			
		||||
                }).toList(),
 | 
			
		||||
                onChanged: (String? newValue) {
 | 
			
		||||
                  if (newValue != null) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _selectedMode = _modes.indexOf(newValue);
 | 
			
		||||
                      _refreshMedia();
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              DropdownButton<int>(
 | 
			
		||||
                value: _crossAxisCount,
 | 
			
		||||
                dropdownColor: const Color.fromARGB(255, 43, 43, 43),
 | 
			
		||||
                iconEnabledColor: Colors.white,
 | 
			
		||||
                items: [0, 3, 4].map((int value) {
 | 
			
		||||
                  return DropdownMenuItem<int>(
 | 
			
		||||
                    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<MediaProvider>(
 | 
			
		||||
          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: <Widget>[
 | 
			
		||||
@@ -289,67 +208,10 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:f0ckapp/models/MediaItem.dart';
 | 
			
		||||
 | 
			
		||||
Future<List<MediaItem>> fetchMedia({
 | 
			
		||||
  String? older,
 | 
			
		||||
  int? older,
 | 
			
		||||
  String? type,
 | 
			
		||||
  int? mode,
 | 
			
		||||
  bool? random,
 | 
			
		||||
@@ -16,7 +16,7 @@ Future<List<MediaItem>> 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(),
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user