- screaming_possum.gif
This commit is contained in:
		@@ -1,78 +1,47 @@
 | 
				
			|||||||
 | 
					import 'package:f0ckapp/providers/theme_provider.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/screens/mediagrid_screen.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:f0ckapp/screens/detailview_screen.dart';
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/screens/settings_screen.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/utils/appversion_util.dart';
 | 
					import 'package:f0ckapp/utils/appversion_util.dart';
 | 
				
			||||||
import 'package:f0ckapp/providers/theme_provider.dart';
 | 
					import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					import 'package:f0ckapp/screens/media_grid.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/settings_screen.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/detailview_screen.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() async {
 | 
					void main() async {
 | 
				
			||||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
 | 
					  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
 | 
				
			||||||
  await AppVersion.init();
 | 
					  await AppVersion.init();
 | 
				
			||||||
 | 
					  final ThemeController themeController = Get.put(ThemeController());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  runApp(ProviderScope(child: F0ckApp()));
 | 
					  final api = ApiService();
 | 
				
			||||||
}
 | 
					  await api.fetchMedia();
 | 
				
			||||||
 | 
					  Get.put(api);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class F0ckApp extends ConsumerWidget {
 | 
					  runApp(
 | 
				
			||||||
  const F0ckApp({super.key});
 | 
					    GetMaterialApp(
 | 
				
			||||||
 | 
					      theme: themeController.currentTheme.value,
 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					 | 
				
			||||||
    final ThemeData theme = ref.watch(themeNotifierProvider);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return MaterialApp(
 | 
					 | 
				
			||||||
      debugShowCheckedModeBanner: false,
 | 
					 | 
				
			||||||
      theme: theme,
 | 
					 | 
				
			||||||
      initialRoute: '/',
 | 
					      initialRoute: '/',
 | 
				
			||||||
      routes: {
 | 
					      getPages: [
 | 
				
			||||||
        '/': (context) => const MediaGrid(),
 | 
					        GetPage(name: '/', page: () => MediaGrid()),
 | 
				
			||||||
        '/settings': (context) => const SettingsPage(),
 | 
					        GetPage(name: '/settings', page: () => SettingsPage()),
 | 
				
			||||||
 | 
					        GetPage(
 | 
				
			||||||
 | 
					          name: '/:itemId',
 | 
				
			||||||
 | 
					          page: () {
 | 
				
			||||||
 | 
					            int? test = int.tryParse(Get.parameters['itemId']!);
 | 
				
			||||||
 | 
					            if (test == null) {
 | 
				
			||||||
 | 
					              return Scaffold(body: Center(child: Text('oof')));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return DetailView(initialItemId: test);
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
      onGenerateRoute: (RouteSettings settings) {
 | 
					        ),
 | 
				
			||||||
        final String? name = settings.name;
 | 
					      ],
 | 
				
			||||||
        if (name == null) {
 | 
					      unknownRoute: GetPage(
 | 
				
			||||||
          return MaterialPageRoute(
 | 
					        name: '/notfound',
 | 
				
			||||||
            builder: (_) =>
 | 
					        page: () => Center(child: Text('oof')),
 | 
				
			||||||
                const Scaffold(body: Center(child: Text('Ungültiger Link'))),
 | 
					      ),
 | 
				
			||||||
            settings: settings,
 | 
					    ),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
        final RegExp regExp = RegExp(
 | 
					 | 
				
			||||||
          r'^(?:/tag/(?<tag>[^/]+))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        final RegExpMatch? match = regExp.firstMatch(name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (match != null) {
 | 
					 | 
				
			||||||
          final String? tag = match.namedGroup('tag');
 | 
					 | 
				
			||||||
          final String? mime = match.namedGroup('mime');
 | 
					 | 
				
			||||||
          final String? idStr = match.namedGroup('itemid');
 | 
					 | 
				
			||||||
          final int? itemId = idStr != null ? int.tryParse(idStr) : null;
 | 
					 | 
				
			||||||
          const int preloadOffset = 50;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (itemId != null) {
 | 
					 | 
				
			||||||
            return MaterialPageRoute(
 | 
					 | 
				
			||||||
              builder: (context) => DetailView(initialItemId: itemId),
 | 
					 | 
				
			||||||
              settings: settings,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return MaterialPageRoute(
 | 
					 | 
				
			||||||
            builder: (context) => const MediaGrid(),
 | 
					 | 
				
			||||||
            settings: settings,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return MaterialPageRoute(
 | 
					 | 
				
			||||||
          builder: (context) =>
 | 
					 | 
				
			||||||
              const Scaffold(body: Center(child: Text('Ungültiger Link'))),
 | 
					 | 
				
			||||||
          settings: settings,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,163 +0,0 @@
 | 
				
			|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/services/api_service.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
					 | 
				
			||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
					 | 
				
			||||||
const _unsetTag = Object();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MediaState {
 | 
					 | 
				
			||||||
  final int typeIndex;
 | 
					 | 
				
			||||||
  final int modeIndex;
 | 
					 | 
				
			||||||
  final bool random;
 | 
					 | 
				
			||||||
  final String? tag;
 | 
					 | 
				
			||||||
  final int crossAxisCount;
 | 
					 | 
				
			||||||
  final List<MediaItem> mediaItems;
 | 
					 | 
				
			||||||
  final bool isLoading;
 | 
					 | 
				
			||||||
  final bool muted;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const MediaState({
 | 
					 | 
				
			||||||
    this.typeIndex = 0,
 | 
					 | 
				
			||||||
    this.modeIndex = 0,
 | 
					 | 
				
			||||||
    this.random = false,
 | 
					 | 
				
			||||||
    this.tag,
 | 
					 | 
				
			||||||
    this.crossAxisCount = 0,
 | 
					 | 
				
			||||||
    this.mediaItems = const [],
 | 
					 | 
				
			||||||
    this.isLoading = false,
 | 
					 | 
				
			||||||
    this.muted = false,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  MediaState replace({
 | 
					 | 
				
			||||||
    int? typeIndex,
 | 
					 | 
				
			||||||
    int? modeIndex,
 | 
					 | 
				
			||||||
    bool? random,
 | 
					 | 
				
			||||||
    Object? tag = _unsetTag,
 | 
					 | 
				
			||||||
    int? crossAxisCount,
 | 
					 | 
				
			||||||
    List<MediaItem>? mediaItems,
 | 
					 | 
				
			||||||
    bool? isLoading,
 | 
					 | 
				
			||||||
    bool? muted,
 | 
					 | 
				
			||||||
  }) {
 | 
					 | 
				
			||||||
    return MediaState(
 | 
					 | 
				
			||||||
      typeIndex: typeIndex ?? this.typeIndex,
 | 
					 | 
				
			||||||
      modeIndex: modeIndex ?? this.modeIndex,
 | 
					 | 
				
			||||||
      random: random ?? this.random,
 | 
					 | 
				
			||||||
      tag: identical(tag, _unsetTag) ? this.tag : tag as String?,
 | 
					 | 
				
			||||||
      crossAxisCount: crossAxisCount ?? this.crossAxisCount,
 | 
					 | 
				
			||||||
      mediaItems: mediaItems ?? this.mediaItems,
 | 
					 | 
				
			||||||
      isLoading: isLoading ?? this.isLoading,
 | 
					 | 
				
			||||||
      muted: muted ?? this.muted,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MediaNotifier extends StateNotifier<MediaState> {
 | 
					 | 
				
			||||||
  final _storage = const FlutterSecureStorage(
 | 
					 | 
				
			||||||
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  MediaNotifier() : super(const MediaState()) {
 | 
					 | 
				
			||||||
    _loadMutedState();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _loadMutedState() async {
 | 
					 | 
				
			||||||
    final storedMuted = await _storage.read(key: 'muted');
 | 
					 | 
				
			||||||
    final isMuted = storedMuted == 'true';
 | 
					 | 
				
			||||||
    state = state.replace(muted: isMuted);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _saveMutedState() async {
 | 
					 | 
				
			||||||
    await _storage.write(key: 'muted', value: state.muted.toString());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void setType(String type) {
 | 
					 | 
				
			||||||
    final newIndex = mediaTypes.indexOf(type);
 | 
					 | 
				
			||||||
    state = state.replace(typeIndex: newIndex);
 | 
					 | 
				
			||||||
    resetMedia();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void setMode(int modeIndex) {
 | 
					 | 
				
			||||||
    state = state.replace(modeIndex: modeIndex);
 | 
					 | 
				
			||||||
    resetMedia();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void toggleRandom() {
 | 
					 | 
				
			||||||
    state = state.replace(random: !state.random);
 | 
					 | 
				
			||||||
    resetMedia();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void setTag(String? tag) {
 | 
					 | 
				
			||||||
    state = state.replace(tag: tag);
 | 
					 | 
				
			||||||
    resetMedia();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void setCrossAxisCount(int count) {
 | 
					 | 
				
			||||||
    state = state.replace(crossAxisCount: count);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void resetMedia() {
 | 
					 | 
				
			||||||
    state = state.replace(mediaItems: []);
 | 
					 | 
				
			||||||
    loadMedia();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void addMediaItems(List<MediaItem> newItems) {
 | 
					 | 
				
			||||||
    final Set<int> existingIds = state.mediaItems
 | 
					 | 
				
			||||||
        .map((item) => item.id)
 | 
					 | 
				
			||||||
        .toSet();
 | 
					 | 
				
			||||||
    final List<MediaItem> filteredItems = newItems
 | 
					 | 
				
			||||||
        .where((item) => !existingIds.contains(item.id))
 | 
					 | 
				
			||||||
        .toList();
 | 
					 | 
				
			||||||
    if (filteredItems.isNotEmpty) {
 | 
					 | 
				
			||||||
      final List<MediaItem> updated = List<MediaItem>.from(state.mediaItems)
 | 
					 | 
				
			||||||
        ..addAll(filteredItems);
 | 
					 | 
				
			||||||
      state = state.replace(mediaItems: updated);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  List<MediaItem> mergeMediaItems(
 | 
					 | 
				
			||||||
    List<MediaItem> current,
 | 
					 | 
				
			||||||
    List<MediaItem> incoming,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    final existingIds = current.map((item) => item.id).toSet();
 | 
					 | 
				
			||||||
    final newItems = incoming
 | 
					 | 
				
			||||||
        .where((item) => !existingIds.contains(item.id))
 | 
					 | 
				
			||||||
        .toList();
 | 
					 | 
				
			||||||
    return [...current, ...newItems];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> loadMedia({int? id}) async {
 | 
					 | 
				
			||||||
    if (state.isLoading) return;
 | 
					 | 
				
			||||||
    state = state.replace(isLoading: true);
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final older =
 | 
					 | 
				
			||||||
          id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
 | 
					 | 
				
			||||||
      final newMedia = await fetchMedia(
 | 
					 | 
				
			||||||
        older: older,
 | 
					 | 
				
			||||||
        type: mediaTypes[state.typeIndex],
 | 
					 | 
				
			||||||
        mode: state.modeIndex,
 | 
					 | 
				
			||||||
        random: state.random,
 | 
					 | 
				
			||||||
        tag: state.tag,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (newMedia.isNotEmpty) {
 | 
					 | 
				
			||||||
        state = state.replace(
 | 
					 | 
				
			||||||
          mediaItems: mergeMediaItems(state.mediaItems, newMedia),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      print('Fehler beim Laden der Medien: $e');
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      state = state.replace(isLoading: false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void toggleMute() {
 | 
					 | 
				
			||||||
    state = state.replace(muted: !state.muted);
 | 
					 | 
				
			||||||
    _saveMutedState();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
final mediaProvider = StateNotifierProvider<MediaNotifier, MediaState>(
 | 
					 | 
				
			||||||
  (ref) => MediaNotifier(),
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
@@ -1,59 +1,8 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
					import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
final Map<String, ThemeData> themeMap = {
 | 
					 | 
				
			||||||
  'f0ck': f0ckTheme,
 | 
					 | 
				
			||||||
  'P1nk': p1nkTheme, // done
 | 
					 | 
				
			||||||
  'Orange': orangeTheme,
 | 
					 | 
				
			||||||
  'Amoled': amoledTheme,
 | 
					 | 
				
			||||||
  'Paper': paperTheme,
 | 
					 | 
				
			||||||
  //'Iced': icedTheme,
 | 
					 | 
				
			||||||
  'f0ck95': f0ck95Theme,
 | 
					 | 
				
			||||||
  'f0ck95d': f0ck95dTheme,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ThemeNotifier extends StateNotifier<ThemeData> {
 | 
					 | 
				
			||||||
  final FlutterSecureStorage secureStorage;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ThemeNotifier({required this.secureStorage}) : super(f0ckTheme) {
 | 
					 | 
				
			||||||
    _loadTheme();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _loadTheme() async {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      String? savedThemeName = await secureStorage.read(key: 'theme');
 | 
					 | 
				
			||||||
      if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
 | 
					 | 
				
			||||||
        state = themeMap[savedThemeName]!;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      debugPrint('Fehler beim Laden des Themes: $error');
 | 
					 | 
				
			||||||
      state = f0ckTheme;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> updateTheme(String themeName) async {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await secureStorage.write(key: 'theme', value: themeName);
 | 
					 | 
				
			||||||
      state = themeMap[themeName] ?? f0ckTheme;
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      debugPrint('Fehler beim Aktualisieren des Themes: $error');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
final themeNotifierProvider = StateNotifierProvider<ThemeNotifier, ThemeData>((
 | 
					 | 
				
			||||||
  ref,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  return ThemeNotifier(
 | 
					 | 
				
			||||||
    secureStorage: const FlutterSecureStorage(
 | 
					 | 
				
			||||||
      aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Definiere deine Themes wie gehabt:
 | 
				
			||||||
final ThemeData f0ckTheme = ThemeData(
 | 
					final ThemeData f0ckTheme = ThemeData(
 | 
				
			||||||
  brightness: Brightness.dark,
 | 
					  brightness: Brightness.dark,
 | 
				
			||||||
  primaryColor: const Color(0xFF9FFF00),
 | 
					  primaryColor: const Color(0xFF9FFF00),
 | 
				
			||||||
@@ -237,7 +186,7 @@ final ThemeData f0ck95Theme = ThemeData(
 | 
				
			|||||||
    backgroundColor: const Color(0xFFE0E0E0),
 | 
					    backgroundColor: const Color(0xFFE0E0E0),
 | 
				
			||||||
    foregroundColor: Colors.black,
 | 
					    foregroundColor: Colors.black,
 | 
				
			||||||
    elevation: 4,
 | 
					    elevation: 4,
 | 
				
			||||||
    centerTitle: true
 | 
					    centerTitle: true,
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  textTheme: const TextTheme(
 | 
					  textTheme: const TextTheme(
 | 
				
			||||||
    bodyLarge: TextStyle(color: Colors.black),
 | 
					    bodyLarge: TextStyle(color: Colors.black),
 | 
				
			||||||
@@ -286,3 +235,56 @@ final ThemeData f0ck95dTheme = ThemeData(
 | 
				
			|||||||
    trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
 | 
					    trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ThemeController extends GetxController {
 | 
				
			||||||
 | 
					  final FlutterSecureStorage secureStorage = const FlutterSecureStorage(
 | 
				
			||||||
 | 
					    aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Rx<ThemeData> currentTheme = f0ckTheme.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Map<String, ThemeData> themeMap = {
 | 
				
			||||||
 | 
					    'f0ck': f0ckTheme,
 | 
				
			||||||
 | 
					    'P1nk': p1nkTheme,
 | 
				
			||||||
 | 
					    'Orange': orangeTheme,
 | 
				
			||||||
 | 
					    'Amoled': amoledTheme,
 | 
				
			||||||
 | 
					    'Paper': paperTheme,
 | 
				
			||||||
 | 
					    'f0ck95': f0ck95Theme,
 | 
				
			||||||
 | 
					    'f0ck95d': f0ck95dTheme,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onInit() {
 | 
				
			||||||
 | 
					    super.onInit();
 | 
				
			||||||
 | 
					    _loadTheme();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _loadTheme() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final String? savedThemeName = await secureStorage.read(key: 'theme');
 | 
				
			||||||
 | 
					      if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
 | 
				
			||||||
 | 
					        currentTheme.value = themeMap[savedThemeName]!;
 | 
				
			||||||
 | 
					        Get.changeTheme(currentTheme.value);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      debugPrint('Fehler beim Laden des Themes: $error');
 | 
				
			||||||
 | 
					      currentTheme.value = f0ckTheme;
 | 
				
			||||||
 | 
					      Get.changeTheme(f0ckTheme);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> updateTheme(String themeName) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await secureStorage.write(key: 'theme', value: themeName);
 | 
				
			||||||
 | 
					      if (themeMap.containsKey(themeName)) {
 | 
				
			||||||
 | 
					        currentTheme.value = themeMap[themeName]!;
 | 
				
			||||||
 | 
					        Get.changeTheme(currentTheme.value);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        currentTheme.value = f0ckTheme;
 | 
				
			||||||
 | 
					        Get.changeTheme(f0ckTheme);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      debugPrint('Fehler beim Aktualisieren des Themes: $error');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,66 +1,103 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/screens/fullscreen_screen.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:share_plus/share_plus.dart';
 | 
					import 'package:share_plus/share_plus.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
					import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/fullscreen_screen.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
					import 'package:f0ckapp/widgets/video_widget.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/smartrefreshindicator_util.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/utils/pagetransformer_util.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DetailView extends ConsumerStatefulWidget {
 | 
					class DetailView extends StatefulWidget {
 | 
				
			||||||
  final int initialItemId;
 | 
					  final int initialItemId;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const DetailView({super.key, required this.initialItemId});
 | 
					  const DetailView({super.key, required this.initialItemId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  ConsumerState<DetailView> createState() => _DetailViewState();
 | 
					  State<DetailView> createState() => _DetailViewState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _DetailViewState extends ConsumerState<DetailView> {
 | 
					class _DetailViewState extends State<DetailView> {
 | 
				
			||||||
 | 
					  final ApiService apiService = Get.find<ApiService>();
 | 
				
			||||||
  PageController? _pageController;
 | 
					  PageController? _pageController;
 | 
				
			||||||
  bool isLoading = false;
 | 
					  Future<void>? _loadingFuture;
 | 
				
			||||||
  int _currentIndex = 0;
 | 
					  int _currentPage = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
  }
 | 
					    if (!_mediaItemExists(widget.initialItemId)) {
 | 
				
			||||||
 | 
					      _loadingFuture = _fetchAndPreloadMedia(widget.initialItemId);
 | 
				
			||||||
  void _preloadAdjacentMedia(int index) async {
 | 
					    } else {
 | 
				
			||||||
    final mediaState = ref.read(mediaProvider);
 | 
					      _initializePageController();
 | 
				
			||||||
    for (int offset in [-1, 1]) {
 | 
					 | 
				
			||||||
      final adjacentIndex = index + offset;
 | 
					 | 
				
			||||||
      if (adjacentIndex >= 0 && adjacentIndex < mediaState.mediaItems.length) {
 | 
					 | 
				
			||||||
        final url = mediaState.mediaItems[adjacentIndex].mediaUrl;
 | 
					 | 
				
			||||||
        if (await DefaultCacheManager().getFileFromCache(url) == null) {
 | 
					 | 
				
			||||||
          await DefaultCacheManager().downloadFile(url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _loadMoreMedia() async {
 | 
					  bool _mediaItemExists(int id) {
 | 
				
			||||||
    if (isLoading) return;
 | 
					    return apiService.mediaItems.any((media) => media.id == id);
 | 
				
			||||||
    setState(() => isLoading = true);
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _fetchAndPreloadMedia(int targetId) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await ref.read(mediaProvider.notifier).loadMedia();
 | 
					      await apiService.setTag(null);
 | 
				
			||||||
 | 
					      await apiService.fetchMedia(id: targetId + 50, reset: false);
 | 
				
			||||||
 | 
					      _initializePageController();
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      _showMsg("Fehler beim Laden der Medien: $e");
 | 
					      _showMsg("Medien konnten nicht geladen werden");
 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      setState(() => isLoading = false);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _initializePageController() {
 | 
				
			||||||
 | 
					    _currentPage = apiService.mediaItems.indexWhere(
 | 
				
			||||||
 | 
					      (media) => media.id == widget.initialItemId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (_currentPage < 0) {
 | 
				
			||||||
 | 
					      _currentPage = 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _pageController = PageController(initialPage: _currentPage)
 | 
				
			||||||
 | 
					      ..addListener(() {
 | 
				
			||||||
 | 
					        setState(() {
 | 
				
			||||||
 | 
					          _currentPage = _pageController!.page!.round();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    setState(() {});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _pageController?.dispose();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MediaItem? _findMediaItem() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return apiService.mediaItems.firstWhere(
 | 
				
			||||||
 | 
					        (media) => media.id == widget.initialItemId,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _downloadMedia(MediaItem item) async {
 | 
				
			||||||
 | 
					    final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
 | 
				
			||||||
 | 
					    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
				
			||||||
 | 
					      'filePath': file.path,
 | 
				
			||||||
 | 
					      'fileName': item.dest,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    success == true
 | 
				
			||||||
 | 
					        ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
				
			||||||
 | 
					        : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _showMsg(String message) {
 | 
					  void _showMsg(String message) {
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
    ScaffoldMessenger.of(context)
 | 
					    ScaffoldMessenger.of(context)
 | 
				
			||||||
@@ -68,70 +105,55 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
				
			|||||||
      ..showSnackBar(SnackBar(content: Text(message)));
 | 
					      ..showSnackBar(SnackBar(content: Text(message)));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _downloadMedia() async {
 | 
					 | 
				
			||||||
    final MediaState mediaState = ref.read(mediaProvider);
 | 
					 | 
				
			||||||
    final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
 | 
					 | 
				
			||||||
    final File file = await DefaultCacheManager().getSingleFile(
 | 
					 | 
				
			||||||
      currentItem.mediaUrl,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
					 | 
				
			||||||
      'filePath': file.path,
 | 
					 | 
				
			||||||
      'fileName': currentItem.dest,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    success == true
 | 
					 | 
				
			||||||
        ? _showMsg(
 | 
					 | 
				
			||||||
            '${currentItem.dest} wurde in Downloads/fApp neigespeichert.',
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        : _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() {
 | 
					 | 
				
			||||||
    _pageController?.dispose();
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
					    if (_loadingFuture != null) {
 | 
				
			||||||
    final int itemIndex = mediaState.mediaItems.indexWhere(
 | 
					      return FutureBuilder<void>(
 | 
				
			||||||
      (item) => item.id == widget.initialItemId,
 | 
					        future: _loadingFuture,
 | 
				
			||||||
    );
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
 | 
					          if (snapshot.connectionState == ConnectionState.waiting) {
 | 
				
			||||||
    if (itemIndex == -1) {
 | 
					 | 
				
			||||||
      Future.microtask(() {
 | 
					 | 
				
			||||||
        ref.read(mediaProvider.notifier).loadMedia(id: widget.initialItemId + 50);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
            return Scaffold(
 | 
					            return Scaffold(
 | 
				
			||||||
        appBar: AppBar(),
 | 
					              appBar: AppBar(title: const Text("Detail")),
 | 
				
			||||||
              body: const Center(child: CircularProgressIndicator()),
 | 
					              body: const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            MediaItem? item = _findMediaItem();
 | 
				
			||||||
 | 
					            if (item == null) {
 | 
				
			||||||
 | 
					              return Scaffold(
 | 
				
			||||||
 | 
					                appBar: AppBar(title: const Text("Detail")),
 | 
				
			||||||
 | 
					                body: const Center(child: Text("f0ck nicht gefunden")),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return _buildDetail();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (_pageController == null) {
 | 
					    MediaItem? existingItem = _findMediaItem();
 | 
				
			||||||
      _pageController = PageController(initialPage: itemIndex);
 | 
					    if (existingItem == null) {
 | 
				
			||||||
      _currentIndex = itemIndex;
 | 
					      return Scaffold(
 | 
				
			||||||
      _pageController!.addListener(() {
 | 
					        appBar: AppBar(title: const Text("Detail")),
 | 
				
			||||||
        setState(() => _currentIndex = _pageController!.page?.round() ?? 0);
 | 
					        body: const Center(child: Text("f0ck nicht gefunden")),
 | 
				
			||||||
      });
 | 
					      );
 | 
				
			||||||
      _preloadAdjacentMedia(itemIndex);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return _buildDetail();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildDetail() {
 | 
				
			||||||
 | 
					    final MediaItem currentItem = apiService.mediaItems[_currentPage];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      endDrawer: EndDrawer(ref: ref),
 | 
					      endDrawer: const EndDrawer(),
 | 
				
			||||||
      endDrawerEnableOpenDragGesture: false,
 | 
					      endDrawerEnableOpenDragGesture: false,
 | 
				
			||||||
      persistentFooterButtons: mediaState.tag != null
 | 
					      persistentFooterButtons: apiService.tag.value != null
 | 
				
			||||||
          ? [
 | 
					          ? [
 | 
				
			||||||
              Center(
 | 
					              Center(
 | 
				
			||||||
                child: InputChip(
 | 
					                child: InputChip(
 | 
				
			||||||
                  label: Text(mediaState.tag!),
 | 
					                  label: Text(apiService.tag.value!),
 | 
				
			||||||
                  onDeleted: () {
 | 
					                  onDeleted: () {
 | 
				
			||||||
                    ref.read(mediaProvider.notifier).setTag(null);
 | 
					                    apiService.setTag(null);
 | 
				
			||||||
                    //context.push('/', extra: true);
 | 
					                    Get.offAllNamed('/');
 | 
				
			||||||
                    Navigator.pushNamed(context, '/');
 | 
					 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -141,54 +163,53 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
				
			|||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
          SliverAppBar(
 | 
					          SliverAppBar(
 | 
				
			||||||
            floating: true,
 | 
					            floating: true,
 | 
				
			||||||
 | 
					            pinned: true,
 | 
				
			||||||
            snap: true,
 | 
					            snap: true,
 | 
				
			||||||
            centerTitle: true,
 | 
					            centerTitle: true,
 | 
				
			||||||
            title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
 | 
					            title: Text('f0ck #${currentItem.id.toString()}'),
 | 
				
			||||||
            leading: IconButton(
 | 
					            leading: IconButton(
 | 
				
			||||||
              icon: const Icon(Icons.arrow_back),
 | 
					              icon: const Icon(Icons.arrow_back),
 | 
				
			||||||
              onPressed: () {
 | 
					              onPressed: () => Get.back(),
 | 
				
			||||||
                Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushNamed(context, '/');
 | 
					 | 
				
			||||||
                //context.canPop() ? context.pop() : context.go('/', extra: true);
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            actions: [
 | 
					            actions: [
 | 
				
			||||||
              IconButton(
 | 
					              IconButton(
 | 
				
			||||||
                icon: const Icon(Icons.fullscreen),
 | 
					                icon: const Icon(Icons.fullscreen),
 | 
				
			||||||
                onPressed: () {
 | 
					                onPressed: () {
 | 
				
			||||||
                  final currentItem = mediaState.mediaItems[_currentIndex];
 | 
					                  Get.to(
 | 
				
			||||||
                  Navigator.of(context).push(
 | 
					                    FullScreenMediaView(item: currentItem),
 | 
				
			||||||
                    MaterialPageRoute(
 | 
					                    fullscreenDialog: true,
 | 
				
			||||||
                      builder: (_) => FullScreenMediaView(item: currentItem),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              IconButton(
 | 
					              IconButton(
 | 
				
			||||||
                icon: const Icon(Icons.download),
 | 
					                icon: const Icon(Icons.download),
 | 
				
			||||||
                onPressed: _downloadMedia,
 | 
					                onPressed: () async {
 | 
				
			||||||
 | 
					                  await _downloadMedia(currentItem);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              PopupMenuButton<String>(
 | 
					              PopupMenuButton<String>(
 | 
				
			||||||
                onSelected: (value) async {
 | 
					                onSelected: (value) async {
 | 
				
			||||||
                  final item = mediaState.mediaItems[_currentIndex];
 | 
					 | 
				
			||||||
                  switch (value) {
 | 
					                  switch (value) {
 | 
				
			||||||
                    case 'media':
 | 
					                    case 'media':
 | 
				
			||||||
                      File file = await DefaultCacheManager().getSingleFile(
 | 
					                      File file = await DefaultCacheManager().getSingleFile(
 | 
				
			||||||
                        item.mediaUrl,
 | 
					                        currentItem.mediaUrl,
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                      Uint8List bytes = await file.readAsBytes();
 | 
					                      Uint8List bytes = await file.readAsBytes();
 | 
				
			||||||
                      final params = ShareParams(
 | 
					                      final params = ShareParams(
 | 
				
			||||||
                        files: [XFile.fromData(bytes, mimeType: item.mime)],
 | 
					                        files: [
 | 
				
			||||||
 | 
					                          XFile.fromData(bytes, mimeType: currentItem.mime),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                      await SharePlus.instance.share(params);
 | 
					                      await SharePlus.instance.share(params);
 | 
				
			||||||
                      break;
 | 
					                      break;
 | 
				
			||||||
                    case 'direct_link':
 | 
					                    case 'direct_link':
 | 
				
			||||||
                      await SharePlus.instance.share(
 | 
					                      await SharePlus.instance.share(
 | 
				
			||||||
                        ShareParams(text: item.mediaUrl),
 | 
					                        ShareParams(text: currentItem.mediaUrl),
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                      break;
 | 
					                      break;
 | 
				
			||||||
                    case 'post_link':
 | 
					                    case 'post_link':
 | 
				
			||||||
                      await SharePlus.instance.share(
 | 
					                      await SharePlus.instance.share(
 | 
				
			||||||
                        ShareParams(text: item.postUrl),
 | 
					                        ShareParams(text: currentItem.postUrl),
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                      break;
 | 
					                      break;
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
@@ -228,58 +249,57 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          SliverPadding(
 | 
					          SliverFillRemaining(
 | 
				
			||||||
            padding: EdgeInsets.zero,
 | 
					            child: PageView.builder(
 | 
				
			||||||
            sliver: SliverFillRemaining(
 | 
					              controller: _pageController,
 | 
				
			||||||
              child: PageTransformer(
 | 
					              itemCount: apiService.mediaItems.length,
 | 
				
			||||||
                controller: _pageController!,
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                pages: mediaState.mediaItems.map((item) {
 | 
					                final MediaItem pageItem = apiService.mediaItems[index];
 | 
				
			||||||
                  int pageIndex = mediaState.mediaItems.indexOf(item);
 | 
					                return AnimatedBuilder(
 | 
				
			||||||
                  return SafeArea(
 | 
					                  animation: _pageController!,
 | 
				
			||||||
                    top: false,
 | 
					                  builder: (context, child) {
 | 
				
			||||||
                    child: SmartRefreshIndicator(
 | 
					                    double value = 0;
 | 
				
			||||||
                      onRefresh: _loadMoreMedia,
 | 
					                    if (_pageController!.position.haveDimensions) {
 | 
				
			||||||
                      child: _buildMediaItem(item, _currentIndex == pageIndex),
 | 
					                      value = (_pageController!.page! - index).abs();
 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                }).toList(),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    double factor = Curves.easeOut.transform(
 | 
				
			||||||
  Widget _buildMediaItem(MediaItem item, bool isActive) {
 | 
					                      1 - value.clamp(0.0, 1.0),
 | 
				
			||||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
					                    );
 | 
				
			||||||
 | 
					                    double scale = 0.8 + factor * 0.2;
 | 
				
			||||||
    return SingleChildScrollView(
 | 
					                    return Transform.scale(scale: scale, child: child);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  child: SafeArea(
 | 
				
			||||||
 | 
					                    top: false,
 | 
				
			||||||
 | 
					                    child: SingleChildScrollView(
 | 
				
			||||||
                      child: Column(
 | 
					                      child: Column(
 | 
				
			||||||
                        children: [
 | 
					                        children: [
 | 
				
			||||||
          if (item.mime.startsWith('image'))
 | 
					                          if (pageItem.mime.startsWith('image'))
 | 
				
			||||||
                            CachedNetworkImage(
 | 
					                            CachedNetworkImage(
 | 
				
			||||||
              imageUrl: item.mediaUrl,
 | 
					                              imageUrl: pageItem.mediaUrl,
 | 
				
			||||||
                              fit: BoxFit.contain,
 | 
					                              fit: BoxFit.contain,
 | 
				
			||||||
              placeholder: (context, url) =>
 | 
					                              placeholder: (context, url) => const Center(
 | 
				
			||||||
                  const Center(child: CircularProgressIndicator()),
 | 
					                                child: CircularProgressIndicator(),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
                              errorWidget: (context, url, error) =>
 | 
					                              errorWidget: (context, url, error) =>
 | 
				
			||||||
                                  const Center(child: Icon(Icons.error)),
 | 
					                                  const Center(child: Icon(Icons.error)),
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                          else
 | 
					                          else
 | 
				
			||||||
            VideoWidget(details: item, isActive: isActive),
 | 
					                            VideoWidget(
 | 
				
			||||||
 | 
					                              details: pageItem,
 | 
				
			||||||
 | 
					                              isActive: index == _currentPage,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
                          const SizedBox(height: 10, width: double.infinity),
 | 
					                          const SizedBox(height: 10, width: double.infinity),
 | 
				
			||||||
                          Wrap(
 | 
					                          Wrap(
 | 
				
			||||||
                            alignment: WrapAlignment.center,
 | 
					                            alignment: WrapAlignment.center,
 | 
				
			||||||
                            spacing: 5.0,
 | 
					                            spacing: 5.0,
 | 
				
			||||||
            children: item.tags.map((tag) {
 | 
					                            children: pageItem.tags.map((tag) {
 | 
				
			||||||
                              return ActionChip(
 | 
					                              return ActionChip(
 | 
				
			||||||
                                onPressed: () {
 | 
					                                onPressed: () {
 | 
				
			||||||
                  if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
 | 
					                                  if (tag.tag == 'sfw' || tag.tag == 'nsfw') {
 | 
				
			||||||
                  setState(() {
 | 
					                                    return;
 | 
				
			||||||
                    mediaNotifier.setTag(tag.tag);
 | 
					                                  }
 | 
				
			||||||
                    Navigator.pushReplacementNamed(context, '/');
 | 
					                                  apiService.setTag(tag.tag);
 | 
				
			||||||
                  });
 | 
					                                  Get.offAllNamed('/');
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                                label: Text(tag.tag),
 | 
					                                label: Text(tag.tag),
 | 
				
			||||||
                                backgroundColor: switch (tag.id) {
 | 
					                                backgroundColor: switch (tag.id) {
 | 
				
			||||||
@@ -287,13 +307,23 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
				
			|||||||
                                  2 => Colors.red,
 | 
					                                  2 => Colors.red,
 | 
				
			||||||
                                  _ => const Color(0xFF090909),
 | 
					                                  _ => const Color(0xFF090909),
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                labelStyle: const TextStyle(color: Colors.white),
 | 
					                                labelStyle: const TextStyle(
 | 
				
			||||||
 | 
					                                  color: Colors.white,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
                              );
 | 
					                              );
 | 
				
			||||||
                            }).toList(),
 | 
					                            }).toList(),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          const SizedBox(height: 20),
 | 
					                          const SizedBox(height: 20),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										134
									
								
								lib/screens/media_grid.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								lib/screens/media_grid.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/media_tile.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MediaGrid extends StatefulWidget {
 | 
				
			||||||
 | 
					  const MediaGrid({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<StatefulWidget> createState() => _MediaGrid();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _MediaGrid extends State<MediaGrid> {
 | 
				
			||||||
 | 
					  final ApiService apiService = Get.find<ApiService>();
 | 
				
			||||||
 | 
					  final ScrollController _scrollController = ScrollController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _scrollController.addListener(() async {
 | 
				
			||||||
 | 
					      if (_scrollController.position.pixels >=
 | 
				
			||||||
 | 
					          _scrollController.position.maxScrollExtent - 300) {
 | 
				
			||||||
 | 
					        await apiService.fetchMedia();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      endDrawer: EndDrawer(),
 | 
				
			||||||
 | 
					      persistentFooterButtons: [
 | 
				
			||||||
 | 
					        Obx(() {
 | 
				
			||||||
 | 
					          if (apiService.tag.value != null) {
 | 
				
			||||||
 | 
					            return Center(
 | 
				
			||||||
 | 
					              child: InputChip(
 | 
				
			||||||
 | 
					                label: Text(apiService.tag.value!),
 | 
				
			||||||
 | 
					                onDeleted: () {
 | 
				
			||||||
 | 
					                  apiService.setTag(null);
 | 
				
			||||||
 | 
					                  Get.offAllNamed('/');
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return SizedBox.shrink();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      body: CustomScrollView(
 | 
				
			||||||
 | 
					        controller: _scrollController,
 | 
				
			||||||
 | 
					        slivers: [
 | 
				
			||||||
 | 
					          SliverAppBar(
 | 
				
			||||||
 | 
					            floating: true,
 | 
				
			||||||
 | 
					            snap: true,
 | 
				
			||||||
 | 
					            title: GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () async {
 | 
				
			||||||
 | 
					                apiService.setTag(null);
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: Row(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  Image.asset(
 | 
				
			||||||
 | 
					                    'assets/images/f0ck_small.webp',
 | 
				
			||||||
 | 
					                    fit: BoxFit.fitHeight,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const SizedBox(width: 10),
 | 
				
			||||||
 | 
					                  const Text('fApp', style: TextStyle(fontSize: 24)),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            actions: [
 | 
				
			||||||
 | 
					              IconButton(
 | 
				
			||||||
 | 
					                icon: const Icon(Icons.search),
 | 
				
			||||||
 | 
					                onPressed: () async {
 | 
				
			||||||
 | 
					                  await showSearch(
 | 
				
			||||||
 | 
					                    context: context,
 | 
				
			||||||
 | 
					                    delegate: CustomSearchDelegate(),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Obx(
 | 
				
			||||||
 | 
					                () => IconButton(
 | 
				
			||||||
 | 
					                  icon: Icon(
 | 
				
			||||||
 | 
					                    apiService.random.value
 | 
				
			||||||
 | 
					                        ? Icons.shuffle_on_outlined
 | 
				
			||||||
 | 
					                        : Icons.shuffle,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onPressed: () {
 | 
				
			||||||
 | 
					                    apiService.toggleRandom();
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Builder(
 | 
				
			||||||
 | 
					                builder: (context) {
 | 
				
			||||||
 | 
					                  return IconButton(
 | 
				
			||||||
 | 
					                    icon: const Icon(Icons.menu),
 | 
				
			||||||
 | 
					                    onPressed: () {
 | 
				
			||||||
 | 
					                      Scaffold.of(context).openEndDrawer();
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          SliverPadding(
 | 
				
			||||||
 | 
					            padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					            sliver: Obx(
 | 
				
			||||||
 | 
					              () => SliverGrid(
 | 
				
			||||||
 | 
					                delegate: SliverChildBuilderDelegate((context, index) {
 | 
				
			||||||
 | 
					                  return MediaTile(item: apiService.mediaItems[index]);
 | 
				
			||||||
 | 
					                }, childCount: apiService.mediaItems.length),
 | 
				
			||||||
 | 
					                gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
				
			||||||
 | 
					                  maxCrossAxisExtent: 150,
 | 
				
			||||||
 | 
					                  crossAxisSpacing: 5,
 | 
				
			||||||
 | 
					                  mainAxisSpacing: 5,
 | 
				
			||||||
 | 
					                  childAspectRatio: 1,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
				
			||||||
 | 
					      floatingActionButton: FloatingActionButton(
 | 
				
			||||||
 | 
					        onPressed: () {},
 | 
				
			||||||
 | 
					        child: Icon(Icons.add),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,162 +0,0 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/providers/media_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});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  ConsumerState<MediaGrid> createState() => _MediaGridState();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
					 | 
				
			||||||
  final ScrollController _scrollController = ScrollController();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void initState() {
 | 
					 | 
				
			||||||
    super.initState();
 | 
					 | 
				
			||||||
    Future.microtask(() {
 | 
					 | 
				
			||||||
      ref.read(mediaProvider.notifier).loadMedia();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    _scrollController.addListener(() {
 | 
					 | 
				
			||||||
      if (_scrollController.position.pixels >=
 | 
					 | 
				
			||||||
          _scrollController.position.maxScrollExtent - 200) {
 | 
					 | 
				
			||||||
        ref.read(mediaProvider.notifier).loadMedia();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() {
 | 
					 | 
				
			||||||
    _scrollController.dispose();
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
					 | 
				
			||||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Scaffold(
 | 
					 | 
				
			||||||
      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)),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              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();
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            SliverPadding(
 | 
					 | 
				
			||||||
              padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
              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: mediaState.crossAxisCount == 0
 | 
					 | 
				
			||||||
                    ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
					 | 
				
			||||||
                        maxCrossAxisExtent: 150,
 | 
					 | 
				
			||||||
                        crossAxisSpacing: 5,
 | 
					 | 
				
			||||||
                        mainAxisSpacing: 5,
 | 
					 | 
				
			||||||
                        childAspectRatio: 1,
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    : SliverGridDelegateWithFixedCrossAxisCount(
 | 
					 | 
				
			||||||
                        crossAxisCount: mediaState.crossAxisCount,
 | 
					 | 
				
			||||||
                        crossAxisSpacing: 5,
 | 
					 | 
				
			||||||
                        mainAxisSpacing: 5,
 | 
					 | 
				
			||||||
                        childAspectRatio: 1,
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      bottomNavigationBar: FilterBar(
 | 
					 | 
				
			||||||
        mediaNotifier: mediaNotifier,
 | 
					 | 
				
			||||||
        mediaState: mediaState,
 | 
					 | 
				
			||||||
        scrollController: _scrollController,
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      endDrawer: EndDrawer(ref: ref),
 | 
					 | 
				
			||||||
      endDrawerEnableOpenDragGesture: false,
 | 
					 | 
				
			||||||
      persistentFooterButtons: mediaState.tag != null
 | 
					 | 
				
			||||||
          ? [
 | 
					 | 
				
			||||||
              Center(
 | 
					 | 
				
			||||||
                child: InputChip(
 | 
					 | 
				
			||||||
                  label: Text(mediaState.tag!),
 | 
					 | 
				
			||||||
                  onDeleted: () {
 | 
					 | 
				
			||||||
                    mediaNotifier.setTag(null);
 | 
					 | 
				
			||||||
                    _scrollController.jumpTo(0);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
          : null,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,30 +1,32 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingsPage extends ConsumerStatefulWidget {
 | 
					class SettingsPage extends StatefulWidget {
 | 
				
			||||||
  const SettingsPage({super.key});
 | 
					  const SettingsPage({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  ConsumerState<SettingsPage> createState() => _SettingsPageState();
 | 
					  State<StatefulWidget> createState() => _SettingsPageState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
 | 
					class _SettingsPageState extends State<SettingsPage> {
 | 
				
			||||||
  int _columns = 3;
 | 
					  int _columns = 3;
 | 
				
			||||||
  bool _drawerSwipeEnabled = true;
 | 
					  bool _drawerSwipeEnabled = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _showMsg(String message, BuildContext context) {
 | 
				
			||||||
 | 
					    ScaffoldMessenger.of(context)
 | 
				
			||||||
 | 
					      ..removeCurrentSnackBar()
 | 
				
			||||||
 | 
					      ..showSnackBar(SnackBar(content: Text(message)));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
					 | 
				
			||||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      endDrawerEnableOpenDragGesture: _drawerSwipeEnabled,
 | 
					      endDrawerEnableOpenDragGesture: _drawerSwipeEnabled,
 | 
				
			||||||
      endDrawer: EndDrawer(ref: ref),
 | 
					      endDrawer: EndDrawer(),
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: CustomScrollView(
 | 
				
			||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
          SliverAppBar(
 | 
					          SliverAppBar(
 | 
				
			||||||
@@ -34,13 +36,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
 | 
				
			|||||||
            leading: IconButton(
 | 
					            leading: IconButton(
 | 
				
			||||||
              icon: const Icon(Icons.arrow_back),
 | 
					              icon: const Icon(Icons.arrow_back),
 | 
				
			||||||
              onPressed: () {
 | 
					              onPressed: () {
 | 
				
			||||||
                Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushReplacementNamed(context, '/');
 | 
					                Get.back();
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          SliverList(
 | 
					          SliverList(
 | 
				
			||||||
            delegate: SliverChildListDelegate([
 | 
					            delegate: SliverChildListDelegate([
 | 
				
			||||||
              Padding(
 | 
					              /*Padding(
 | 
				
			||||||
                padding: const EdgeInsets.all(16.0),
 | 
					                padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                child: Text(
 | 
					                child: Text(
 | 
				
			||||||
                  "Anzahl der Spalten",
 | 
					                  "Anzahl der Spalten",
 | 
				
			||||||
@@ -68,7 +70,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),*/
 | 
				
			||||||
              const Divider(),
 | 
					              const Divider(),
 | 
				
			||||||
              SwitchListTile(
 | 
					              SwitchListTile(
 | 
				
			||||||
                title: const Text("Drawer per Geste öffnen"),
 | 
					                title: const Text("Drawer per Geste öffnen"),
 | 
				
			||||||
@@ -84,14 +86,12 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
              const Divider(),
 | 
					              const Divider(),
 | 
				
			||||||
              ListTile(
 | 
					              ListTile(
 | 
				
			||||||
                title: const Text("Cache löschen"),
 | 
					                title: Text("Cache löschen"),
 | 
				
			||||||
                trailing: ElevatedButton(
 | 
					                trailing: ElevatedButton(
 | 
				
			||||||
                  onPressed: () async {
 | 
					                  onPressed: () async {
 | 
				
			||||||
                    await DefaultCacheManager().emptyCache();
 | 
					                    await DefaultCacheManager().emptyCache();
 | 
				
			||||||
                    if (!mounted) return;
 | 
					                    if (!mounted) return;
 | 
				
			||||||
                    ScaffoldMessenger.of(context).showSnackBar(
 | 
					                    _showMsg('Cache wurde geleert.', context);
 | 
				
			||||||
                      const SnackBar(content: Text("Cache wurde geleert.")),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  child: const Text("Löschen"),
 | 
					                  child: const Text("Löschen"),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,111 +1,94 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'dart:convert';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
					import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
				
			||||||
import 'package:f0ckapp/models/suggestion_model.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
final FlutterSecureStorage storage = const FlutterSecureStorage(
 | 
					const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
				
			||||||
  aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
					const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<List<MediaItem>> fetchMedia({
 | 
					class ApiService extends GetConnect {
 | 
				
			||||||
  int? older,
 | 
					  RxList<MediaItem> mediaItems = <MediaItem>[].obs;
 | 
				
			||||||
  String type = 'image',
 | 
					  RxnString tag = RxnString();
 | 
				
			||||||
  int mode = 0,
 | 
					  RxInt type = 0.obs;
 | 
				
			||||||
  bool random = false,
 | 
					  RxInt mode = 0.obs;
 | 
				
			||||||
  String? tag,
 | 
					  RxBool random = false.obs;
 | 
				
			||||||
}) async {
 | 
					
 | 
				
			||||||
 | 
					  bool _isFetching = false;
 | 
				
			||||||
 | 
					  DateTime? _lastFetchTime;
 | 
				
			||||||
 | 
					  final Duration _minFetchInterval = Duration(milliseconds: 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onInit() {
 | 
				
			||||||
 | 
					    super.onInit();
 | 
				
			||||||
 | 
					    everAll([tag, type, mode, random], (_) => fetchMedia(reset: true));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setTag(String? newTag) async {
 | 
				
			||||||
 | 
					    tag.value = newTag;
 | 
				
			||||||
 | 
					    return await fetchMedia(reset: true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setType(int newType) async {
 | 
				
			||||||
 | 
					    type.value = newType >= 0 && newType < mediaTypes.length ? newType : 0;
 | 
				
			||||||
 | 
					    return await fetchMedia(reset: true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setMode(int newMode) async {
 | 
				
			||||||
 | 
					    mode.value = newMode >= 0 && newMode < mediaModes.length ? newMode : 0;
 | 
				
			||||||
 | 
					    return await fetchMedia(reset: true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> toggleRandom() async {
 | 
				
			||||||
 | 
					    random.value = !random.value;
 | 
				
			||||||
 | 
					    return await fetchMedia(reset: true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future fetchMedia({int? id, bool reset = false}) async {
 | 
				
			||||||
 | 
					    if (!reset) {
 | 
				
			||||||
 | 
					      if (_isFetching) return;
 | 
				
			||||||
 | 
					      if (_lastFetchTime != null &&
 | 
				
			||||||
 | 
					          DateTime.now().difference(_lastFetchTime!) < _minFetchInterval) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _lastFetchTime = DateTime.now();
 | 
				
			||||||
 | 
					    _isFetching = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (reset) mediaItems.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final int? older =
 | 
				
			||||||
 | 
					        id ?? (mediaItems.isNotEmpty ? mediaItems.last.id : null);
 | 
				
			||||||
    final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace(
 | 
					    final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace(
 | 
				
			||||||
      queryParameters: {
 | 
					      queryParameters: {
 | 
				
			||||||
      'type': type,
 | 
					        'type': mediaTypes[type.value],
 | 
				
			||||||
      'mode': mode.toString(),
 | 
					        'mode': mode.value.toString(),
 | 
				
			||||||
      'random': (random ? 1 : 0).toString(),
 | 
					        'random': (random.value ? 1 : 0).toString(),
 | 
				
			||||||
      if (tag != null) 'tag': tag,
 | 
					        if (tag.value != null) 'tag': tag.value,
 | 
				
			||||||
        if (older != null) 'older': older.toString(),
 | 
					        if (older != null) 'older': older.toString(),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final http.Response response = await http.get(url);
 | 
					 | 
				
			||||||
  if (response.statusCode == 200) {
 | 
					 | 
				
			||||||
    final List<dynamic> jsonList = jsonDecode(response.body);
 | 
					 | 
				
			||||||
    return jsonList.map((item) => MediaItem.fromJson(item)).toList();
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    throw Exception('Fehler beim Abrufen der Medien: ${response.statusCode}');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Future<MediaItem> fetchMediaDetail(int itemId) async {
 | 
					 | 
				
			||||||
  final Uri url = Uri.parse('https://api.f0ck.me/item/${itemId.toString()}');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final http.Response response = await http.get(url);
 | 
					 | 
				
			||||||
  if (response.statusCode == 200) {
 | 
					 | 
				
			||||||
    final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return MediaItem.fromJson(jsonResponse);
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    throw Exception(
 | 
					 | 
				
			||||||
      'Fehler beim Abrufen der Media-Details: ${response.statusCode}',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Future<List<Suggestion>> fetchSuggestions(String query) async {
 | 
					 | 
				
			||||||
  final Uri uri = Uri.parse('https://api.f0ck.me/search/?q=$query');
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
    final http.Response response = await http
 | 
					      final Response<dynamic> response = await get(url.toString());
 | 
				
			||||||
        .get(uri)
 | 
					 | 
				
			||||||
        .timeout(const Duration(seconds: 5));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (response.statusCode == 200) {
 | 
					      if (response.isOk && response.body is List) {
 | 
				
			||||||
      final dynamic decoded = jsonDecode(response.body);
 | 
					        List<MediaItem> newMedia = (response.body as List)
 | 
				
			||||||
      if (decoded is List) {
 | 
					            .map((json) => MediaItem.fromJson(json))
 | 
				
			||||||
        final suggestions = decoded
 | 
					 | 
				
			||||||
            .map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
 | 
					 | 
				
			||||||
            .toList();
 | 
					            .toList();
 | 
				
			||||||
        suggestions.sort((a, b) => b.score.compareTo(a.score));
 | 
					
 | 
				
			||||||
        return suggestions;
 | 
					        if (reset) {
 | 
				
			||||||
 | 
					          mediaItems.assignAll(newMedia);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
        throw Exception('Unerwartetes Format: Es wurde eine Liste erwartet.');
 | 
					          mediaItems.addAll(newMedia);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else if (response.statusCode == 400) {
 | 
					 | 
				
			||||||
      final dynamic error = jsonDecode(response.body);
 | 
					 | 
				
			||||||
      final String message = error is Map<String, dynamic>
 | 
					 | 
				
			||||||
          ? error['detail']?.toString() ?? 'Unbekannter Fehler.'
 | 
					 | 
				
			||||||
          : 'Unbekannter Fehler.';
 | 
					 | 
				
			||||||
      throw Exception('Client-Fehler 400: $message');
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
      throw Exception(
 | 
					        debugPrint('Fehler beim Laden der MediaItems: ${response.statusText}');
 | 
				
			||||||
        'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
  } on TimeoutException {
 | 
					 | 
				
			||||||
    throw Exception('Anfrage an die API hat zu lange gedauert.');
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
    throw Exception('Fehler bei der Verarbeitung der Anfrage: $e');
 | 
					      debugPrint('Exception beim Laden der MediaItems: $e');
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      _isFetching = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
Future<bool> login(String username, String password) async {
 | 
					 | 
				
			||||||
  final Uri url = Uri.parse('https://api.f0ck.me/login');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final http.Response response = await http.post(
 | 
					 | 
				
			||||||
    url,
 | 
					 | 
				
			||||||
    body: {'username': username, 'password': password},
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (response.statusCode == 200) {
 | 
					 | 
				
			||||||
    final dynamic data = jsonDecode(response.body);
 | 
					 | 
				
			||||||
    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 {
 | 
					 | 
				
			||||||
    throw Exception('Login fehlgeschlagen: ${response.statusCode}');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,16 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/services/api_service.dart';
 | 
					import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
import 'package:f0ckapp/models/suggestion_model.dart';
 | 
					import 'package:f0ckapp/models/suggestion_model.dart';
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CustomSearchDelegate extends SearchDelegate<String> {
 | 
					class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			||||||
 | 
					  final ApiService apiService = Get.find<ApiService>();
 | 
				
			||||||
  Timer? _debounceTimer;
 | 
					  Timer? _debounceTimer;
 | 
				
			||||||
  List<Suggestion>? _suggestions;
 | 
					  List<Suggestion>? _suggestions;
 | 
				
			||||||
  bool _isLoading = false;
 | 
					  bool _isLoading = false;
 | 
				
			||||||
@@ -42,13 +47,52 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			|||||||
    return Center(child: Text('Suchergebnisse für: "$query"'));
 | 
					    return Center(child: Text('Suchergebnisse für: "$query"'));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<Suggestion>> fetchSuggestions(String query) async {
 | 
				
			||||||
 | 
					    final Uri uri = Uri.parse('https://api.f0ck.me/search/?q=$query');
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final http.Response response = await http
 | 
				
			||||||
 | 
					          .get(uri)
 | 
				
			||||||
 | 
					          .timeout(const Duration(seconds: 5));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (response.statusCode == 200) {
 | 
				
			||||||
 | 
					        final dynamic decoded = jsonDecode(response.body);
 | 
				
			||||||
 | 
					        if (decoded is List) {
 | 
				
			||||||
 | 
					          final suggestions = decoded
 | 
				
			||||||
 | 
					              .map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
 | 
				
			||||||
 | 
					              .toList();
 | 
				
			||||||
 | 
					          suggestions.sort((a, b) => b.score.compareTo(a.score));
 | 
				
			||||||
 | 
					          return suggestions;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw Exception('Unerwartetes Format: Es wurde eine Liste erwartet.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (response.statusCode == 400) {
 | 
				
			||||||
 | 
					        final dynamic error = jsonDecode(response.body);
 | 
				
			||||||
 | 
					        final String message = error is Map<String, dynamic>
 | 
				
			||||||
 | 
					            ? error['detail']?.toString() ?? 'Unbekannter Fehler.'
 | 
				
			||||||
 | 
					            : 'Unbekannter Fehler.';
 | 
				
			||||||
 | 
					        throw Exception('Client-Fehler 400: $message');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        throw Exception(
 | 
				
			||||||
 | 
					          'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } on TimeoutException {
 | 
				
			||||||
 | 
					      throw Exception('Anfrage an die API hat zu lange gedauert.');
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      throw Exception('Fehler bei der Verarbeitung der Anfrage: $e');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget buildSuggestions(BuildContext context) {
 | 
					  Widget buildSuggestions(BuildContext context) {
 | 
				
			||||||
    return StatefulBuilder(
 | 
					    return StatefulBuilder(
 | 
				
			||||||
      builder: (BuildContext context, void Function(void Function()) setState) {
 | 
					      builder: (BuildContext context, void Function(void Function()) setState) {
 | 
				
			||||||
        if (query.isEmpty) {
 | 
					        if (query.isEmpty) {
 | 
				
			||||||
          _debounceTimer?.cancel();
 | 
					          _debounceTimer?.cancel();
 | 
				
			||||||
          return Container(padding: const EdgeInsets.all(16.0), child: const Text(''));
 | 
					          return Container(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					            child: const Text(''),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (query != _lastFetchedQuery) {
 | 
					        if (query != _lastFetchedQuery) {
 | 
				
			||||||
@@ -90,8 +134,6 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			|||||||
          return Center(child: const Text("Keine Ergebnisse gefunden."));
 | 
					          return Center(child: const Text("Keine Ergebnisse gefunden."));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Consumer(
 | 
					 | 
				
			||||||
          builder: (BuildContext context, WidgetRef ref, Widget? child) {
 | 
					 | 
				
			||||||
        return ListView.builder(
 | 
					        return ListView.builder(
 | 
				
			||||||
          itemCount: _suggestions!.length,
 | 
					          itemCount: _suggestions!.length,
 | 
				
			||||||
          itemBuilder: (BuildContext context, int index) {
 | 
					          itemBuilder: (BuildContext context, int index) {
 | 
				
			||||||
@@ -102,8 +144,8 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			|||||||
                'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
 | 
					                'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
 | 
				
			||||||
                style: TextStyle(fontSize: 12),
 | 
					                style: TextStyle(fontSize: 12),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
                  onTap: () {
 | 
					              onTap: () async {
 | 
				
			||||||
                    ref.read(mediaProvider.notifier).setTag(suggestion.tag);
 | 
					                await apiService.setTag(suggestion.tag);
 | 
				
			||||||
                close(context, suggestion.tag);
 | 
					                close(context, suggestion.tag);
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
@@ -111,8 +153,6 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget _buildLoadingIndicator() {
 | 
					  Widget _buildLoadingIndicator() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,42 +0,0 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class PageTransformer extends StatelessWidget {
 | 
					 | 
				
			||||||
  final List<Widget> pages;
 | 
					 | 
				
			||||||
  final PageController controller;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const PageTransformer({
 | 
					 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.pages,
 | 
					 | 
				
			||||||
    required this.controller,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return PageView.builder(
 | 
					 | 
				
			||||||
      controller: controller,
 | 
					 | 
				
			||||||
      itemCount: pages.length,
 | 
					 | 
				
			||||||
      itemBuilder: (context, index) {
 | 
					 | 
				
			||||||
        return _buildPage(pages[index], index);
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Widget _buildPage(Widget page, int index) {
 | 
					 | 
				
			||||||
    return AnimatedBuilder(
 | 
					 | 
				
			||||||
      animation: controller,
 | 
					 | 
				
			||||||
      builder: (context, child) {
 | 
					 | 
				
			||||||
        double value = 1.0;
 | 
					 | 
				
			||||||
        if (controller.position.haveDimensions) {
 | 
					 | 
				
			||||||
          value = controller.page! - index;
 | 
					 | 
				
			||||||
          value = (1 - (value.abs() * 0.5)).clamp(0.0, 1.0);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Transform(
 | 
					 | 
				
			||||||
          transform: Matrix4.identity()..scale(value, value),
 | 
					 | 
				
			||||||
          alignment: Alignment.center,
 | 
					 | 
				
			||||||
          child: child,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      child: page,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,14 +1,13 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/providers/theme_provider.dart';
 | 
					import 'package:f0ckapp/providers/theme_provider.dart';
 | 
				
			||||||
 | 
					//import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/appversion_util.dart';
 | 
					import 'package:f0ckapp/utils/appversion_util.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EndDrawer extends StatelessWidget {
 | 
					class EndDrawer extends StatelessWidget {
 | 
				
			||||||
  final WidgetRef ref;
 | 
					  const EndDrawer({super.key});
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const EndDrawer({super.key, required this.ref});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _showMsg(String message, BuildContext context) {
 | 
					  void _showMsg(String message, BuildContext context) {
 | 
				
			||||||
    ScaffoldMessenger.of(context)
 | 
					    ScaffoldMessenger.of(context)
 | 
				
			||||||
@@ -18,6 +17,9 @@ class EndDrawer extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    //final ApiService c = Get.find<ApiService>();
 | 
				
			||||||
 | 
					    final ThemeController themeController = Get.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Drawer(
 | 
					    return Drawer(
 | 
				
			||||||
      child: ListView(
 | 
					      child: ListView(
 | 
				
			||||||
        padding: EdgeInsets.zero,
 | 
					        padding: EdgeInsets.zero,
 | 
				
			||||||
@@ -88,34 +90,32 @@ class EndDrawer extends StatelessWidget {
 | 
				
			|||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              Padding(
 | 
					              Padding(
 | 
				
			||||||
                padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
					                padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
				
			||||||
                child: Column(
 | 
					                child: Obx(() {
 | 
				
			||||||
                  children: themeMap.entries.map((entry) {
 | 
					                  return Column(
 | 
				
			||||||
 | 
					                    children: themeController.themeMap.entries.map((entry) {
 | 
				
			||||||
                      final String themeName = entry.key;
 | 
					                      final String themeName = entry.key;
 | 
				
			||||||
                      final ThemeData themeData = entry.value;
 | 
					                      final ThemeData themeData = entry.value;
 | 
				
			||||||
                    final ThemeData currentTheme = ref.watch(
 | 
					                      final bool isSelected =
 | 
				
			||||||
                      themeNotifierProvider,
 | 
					                          themeController.currentTheme.value == themeData;
 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                    final bool isSelected = currentTheme == themeData;
 | 
					 | 
				
			||||||
                      return ListTile(
 | 
					                      return ListTile(
 | 
				
			||||||
                        title: Text(themeName),
 | 
					                        title: Text(themeName),
 | 
				
			||||||
                        selected: isSelected,
 | 
					                        selected: isSelected,
 | 
				
			||||||
                        selectedTileColor: Colors.blue.withValues(alpha: 0.2),
 | 
					                        selectedTileColor: Colors.blue.withValues(alpha: 0.2),
 | 
				
			||||||
                        onTap: () async {
 | 
					                        onTap: () async {
 | 
				
			||||||
                        await ref
 | 
					                          await themeController.updateTheme(themeName);
 | 
				
			||||||
                            .read(themeNotifierProvider.notifier)
 | 
					 | 
				
			||||||
                            .updateTheme(themeName);
 | 
					 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    }).toList(),
 | 
					                    }).toList(),
 | 
				
			||||||
                ),
 | 
					                  );
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
            title: const Text('Einstellungen'),
 | 
					            title: const Text('Settings'),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              //context.go('/settings');
 | 
					              Navigator.pop(context);
 | 
				
			||||||
              Navigator.pushReplacementNamed(context, '/settings');
 | 
					              Get.toNamed('/settings');
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +1,29 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/services/api_service.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FilterBar extends StatelessWidget {
 | 
					class FilterBar extends StatelessWidget {
 | 
				
			||||||
  final MediaState mediaState;
 | 
					 | 
				
			||||||
  final MediaNotifier mediaNotifier;
 | 
					 | 
				
			||||||
  final ScrollController scrollController;
 | 
					  final ScrollController scrollController;
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  const FilterBar({
 | 
					  const FilterBar({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.mediaState,
 | 
					 | 
				
			||||||
    required this.mediaNotifier,
 | 
					 | 
				
			||||||
    required this.scrollController,
 | 
					    required this.scrollController,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final ApiService c = Get.find<ApiService>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return BottomAppBar(
 | 
					    return BottomAppBar(
 | 
				
			||||||
      height: 50,
 | 
					      height: 50,
 | 
				
			||||||
      child: Row(
 | 
					      child: Row(
 | 
				
			||||||
        mainAxisAlignment: MainAxisAlignment.spaceAround,
 | 
					        mainAxisAlignment: MainAxisAlignment.spaceAround,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          const Text('type: '),
 | 
					          const Text('type: '),
 | 
				
			||||||
          DropdownButton<String>(
 | 
					          Obx(() => DropdownButton<String>(
 | 
				
			||||||
            value: mediaTypes[mediaState.typeIndex],
 | 
					            value: mediaTypes[c.type.value],
 | 
				
			||||||
            isDense: true,
 | 
					            isDense: true,
 | 
				
			||||||
            items: mediaTypes.map((String value) {
 | 
					            items: mediaTypes.map((String value) {
 | 
				
			||||||
              return DropdownMenuItem<String>(
 | 
					              return DropdownMenuItem<String>(
 | 
				
			||||||
@@ -33,14 +33,14 @@ class FilterBar extends StatelessWidget {
 | 
				
			|||||||
            }).toList(),
 | 
					            }).toList(),
 | 
				
			||||||
            onChanged: (String? newValue) {
 | 
					            onChanged: (String? newValue) {
 | 
				
			||||||
              if (newValue != null) {
 | 
					              if (newValue != null) {
 | 
				
			||||||
                mediaNotifier.setType(newValue);
 | 
					                c.setType(mediaTypes.indexOf(newValue));
 | 
				
			||||||
                scrollController.jumpTo(0);
 | 
					                scrollController.jumpTo(0);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          )),
 | 
				
			||||||
          const Text('mode: '),
 | 
					          const Text('mode: '),
 | 
				
			||||||
          DropdownButton<String>(
 | 
					          Obx(() => DropdownButton<String>(
 | 
				
			||||||
            value: mediaModes[mediaState.modeIndex],
 | 
					            value: mediaModes[c.mode.value],
 | 
				
			||||||
            isDense: true,
 | 
					            isDense: true,
 | 
				
			||||||
            items: mediaModes.map((String value) {
 | 
					            items: mediaModes.map((String value) {
 | 
				
			||||||
              return DropdownMenuItem<String>(
 | 
					              return DropdownMenuItem<String>(
 | 
				
			||||||
@@ -50,11 +50,11 @@ class FilterBar extends StatelessWidget {
 | 
				
			|||||||
            }).toList(),
 | 
					            }).toList(),
 | 
				
			||||||
            onChanged: (String? newValue) {
 | 
					            onChanged: (String? newValue) {
 | 
				
			||||||
              if (newValue != null) {
 | 
					              if (newValue != null) {
 | 
				
			||||||
                mediaNotifier.setMode(mediaModes.indexOf(newValue));
 | 
					                c.setMode(mediaModes.indexOf(newValue));
 | 
				
			||||||
                scrollController.jumpTo(0);
 | 
					                scrollController.jumpTo(0);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          )),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
					import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,7 +14,8 @@ class MediaTile extends StatelessWidget {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return InkWell(
 | 
					    return InkWell(
 | 
				
			||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        Navigator.pushNamed(context, '/${item.id}');
 | 
					        //Navigator.pushNamed(context, '/${item.id}');
 | 
				
			||||||
 | 
					        Get.toNamed('/${item.id}');
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      child: Stack(
 | 
					      child: Stack(
 | 
				
			||||||
        fit: StackFit.expand,
 | 
					        fit: StackFit.expand,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,11 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
					import 'package:f0ckapp/models/mediaitem_model.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/videooverlay_widget.dart';
 | 
					import 'package:f0ckapp/widgets/videooverlay_widget.dart';
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoWidget extends ConsumerStatefulWidget {
 | 
					class VideoWidget extends StatefulWidget {
 | 
				
			||||||
  final MediaItem details;
 | 
					  final MediaItem details;
 | 
				
			||||||
  final bool isActive;
 | 
					  final bool isActive;
 | 
				
			||||||
  final bool fullScreen;
 | 
					  final bool fullScreen;
 | 
				
			||||||
@@ -23,10 +21,10 @@ class VideoWidget extends ConsumerStatefulWidget {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  ConsumerState<VideoWidget> createState() => _VideoWidgetState();
 | 
					  State<VideoWidget> createState() => _VideoWidgetState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _VideoWidgetState extends ConsumerState<VideoWidget> {
 | 
					class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			||||||
  late CachedVideoPlayerPlusController _controller;
 | 
					  late CachedVideoPlayerPlusController _controller;
 | 
				
			||||||
  bool _showControls = false;
 | 
					  bool _showControls = false;
 | 
				
			||||||
  Timer? _hideControlsTimer;
 | 
					  Timer? _hideControlsTimer;
 | 
				
			||||||
@@ -51,8 +49,8 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    _controller.setLooping(true);
 | 
					    _controller.setLooping(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final bool muted = ref.read(mediaProvider).muted;
 | 
					    //final bool muted = ref.read(mediaProvider).muted;
 | 
				
			||||||
    _controller.setVolume(muted ? 0.0 : 1.0);
 | 
					    //_controller.setVolume(muted ? 0.0 : 1.0);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -88,11 +86,11 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final bool muted = ref.watch(mediaProvider).muted;
 | 
					    //final bool muted = ref.watch(mediaProvider).muted;
 | 
				
			||||||
    if (_controller.value.isInitialized &&
 | 
					    //if (_controller.value.isInitialized &&
 | 
				
			||||||
        _controller.value.volume != (muted ? 0.0 : 1.0)) {
 | 
					    //    _controller.value.volume != (muted ? 0.0 : 1.0)) {
 | 
				
			||||||
      _controller.setVolume(muted ? 0.0 : 1.0);
 | 
					    //  _controller.setVolume(muted ? 0.0 : 1.0);
 | 
				
			||||||
    }
 | 
					    //}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
					    bool isAudio = widget.details.mime.startsWith('audio');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,8 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
					class VideoControlsOverlay extends StatelessWidget {
 | 
				
			||||||
 | 
					 | 
				
			||||||
class VideoControlsOverlay extends ConsumerWidget {
 | 
					 | 
				
			||||||
  final CachedVideoPlayerPlusController controller;
 | 
					  final CachedVideoPlayerPlusController controller;
 | 
				
			||||||
  final VoidCallback button;
 | 
					  final VoidCallback button;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,25 +13,11 @@ class VideoControlsOverlay extends ConsumerWidget {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, ref) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
					 | 
				
			||||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      alignment: Alignment.center,
 | 
					      alignment: Alignment.center,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Positioned(
 | 
					 | 
				
			||||||
          right: 12,
 | 
					 | 
				
			||||||
          bottom: 12,
 | 
					 | 
				
			||||||
          child: _ControlButton(
 | 
					 | 
				
			||||||
            mediaState.muted ? Icons.volume_off : Icons.volume_up,
 | 
					 | 
				
			||||||
            () {
 | 
					 | 
				
			||||||
              button();
 | 
					 | 
				
			||||||
              mediaNotifier.toggleMute();
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            size: 16,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        Center(
 | 
					        Center(
 | 
				
			||||||
          child: Row(
 | 
					          child: Row(
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
					            mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -158,14 +158,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.0.0"
 | 
					    version: "6.0.0"
 | 
				
			||||||
  flutter_riverpod:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_riverpod
 | 
					 | 
				
			||||||
      sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.6.1"
 | 
					 | 
				
			||||||
  flutter_secure_storage:
 | 
					  flutter_secure_storage:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -225,7 +217,7 @@ packages:
 | 
				
			|||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
  get:
 | 
					  get:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: get
 | 
					      name: get
 | 
				
			||||||
      sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425
 | 
					      sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425
 | 
				
			||||||
@@ -432,14 +424,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.8"
 | 
					    version: "2.1.8"
 | 
				
			||||||
  riverpod:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: riverpod
 | 
					 | 
				
			||||||
      sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.6.1"
 | 
					 | 
				
			||||||
  rxdart:
 | 
					  rxdart:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -533,14 +517,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.12.1"
 | 
					    version: "1.12.1"
 | 
				
			||||||
  state_notifier:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: state_notifier
 | 
					 | 
				
			||||||
      sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "1.0.0"
 | 
					 | 
				
			||||||
  stream_channel:
 | 
					  stream_channel:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    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
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 1.1.23+53
 | 
					version: 1.2.0+54
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.9.0-100.2.beta
 | 
					  sdk: ^3.9.0-100.2.beta
 | 
				
			||||||
@@ -40,7 +40,7 @@ dependencies:
 | 
				
			|||||||
  package_info_plus: ^8.3.0
 | 
					  package_info_plus: ^8.3.0
 | 
				
			||||||
  share_plus: ^11.0.0
 | 
					  share_plus: ^11.0.0
 | 
				
			||||||
  flutter_secure_storage: ^9.2.4
 | 
					  flutter_secure_storage: ^9.2.4
 | 
				
			||||||
  flutter_riverpod: ^2.6.1
 | 
					  get: ^4.7.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,12 +8,12 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_test/flutter_test.dart';
 | 
					import 'package:flutter_test/flutter_test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/main.dart';
 | 
					//import 'package:f0ckapp/main.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
 | 
					  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
 | 
				
			||||||
    // Build our app and trigger a frame.
 | 
					    // Build our app and trigger a frame.
 | 
				
			||||||
    await tester.pumpWidget(F0ckApp());
 | 
					    //await tester.pumpWidget(F0ckApp());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Verify that our counter starts at 0.
 | 
					    // Verify that our counter starts at 0.
 | 
				
			||||||
    expect(find.text('0'), findsOneWidget);
 | 
					    expect(find.text('0'), findsOneWidget);
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user