This commit is contained in:
		@@ -6,6 +6,13 @@ import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
 | 
			
		||||
const Map<String, Locale> supportedLocales = {
 | 
			
		||||
  'en_US': Locale('en', 'US'),
 | 
			
		||||
  'de_DE': Locale('de', 'DE'),
 | 
			
		||||
  'fr_FR': Locale('fr', 'FR'),
 | 
			
		||||
  'nl_NL': Locale('nl', 'NL'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MyTranslations extends Translations {
 | 
			
		||||
  static final MyTranslations instance = MyTranslations._internal();
 | 
			
		||||
  MyTranslations._internal();
 | 
			
		||||
@@ -13,15 +20,18 @@ class MyTranslations extends Translations {
 | 
			
		||||
  static final Map<String, Map<String, String>> _translations = {};
 | 
			
		||||
 | 
			
		||||
  static Future<void> loadTranslations() async {
 | 
			
		||||
    final locales = ['en_US', 'de_DE', 'fr_FR', 'nl_NL'];
 | 
			
		||||
    for (final locale in locales) {
 | 
			
		||||
      final String jsonString = await rootBundle.loadString(
 | 
			
		||||
        'assets/i18n/$locale.json',
 | 
			
		||||
      );
 | 
			
		||||
      final Map<String, dynamic> jsonMap = json.decode(jsonString);
 | 
			
		||||
      _translations[locale] = jsonMap.map(
 | 
			
		||||
        (key, value) => MapEntry(key, value.toString()),
 | 
			
		||||
      );
 | 
			
		||||
    for (final localeKey in supportedLocales.keys) {
 | 
			
		||||
      try {
 | 
			
		||||
        final String jsonString = await rootBundle.loadString(
 | 
			
		||||
          'assets/i18n/$localeKey.json',
 | 
			
		||||
        );
 | 
			
		||||
        final Map<String, dynamic> jsonMap = json.decode(jsonString);
 | 
			
		||||
        _translations[localeKey] = jsonMap.map(
 | 
			
		||||
          (key, value) => MapEntry(key, value.toString()),
 | 
			
		||||
        );
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        debugPrint('Konnte Übersetzung für $localeKey nicht laden: $e');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +42,7 @@ class MyTranslations extends Translations {
 | 
			
		||||
class LocalizationController extends GetxController {
 | 
			
		||||
  final EncryptedSharedPreferencesAsync storage =
 | 
			
		||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
			
		||||
  Rx<Locale> currentLocale = const Locale('en', 'US').obs;
 | 
			
		||||
  Rx<Locale> currentLocale = supportedLocales['en_US']!.obs;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onInit() {
 | 
			
		||||
@@ -41,25 +51,29 @@ class LocalizationController extends GetxController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> loadLocale() async {
 | 
			
		||||
    String? savedLocale = await storage.getString(
 | 
			
		||||
    String? savedLocaleKey = await storage.getString(
 | 
			
		||||
      'locale',
 | 
			
		||||
      defaultValue: 'en_US',
 | 
			
		||||
    );
 | 
			
		||||
    if (savedLocale != null && savedLocale.isNotEmpty) {
 | 
			
		||||
      final List<String> parts = savedLocale.split('_');
 | 
			
		||||
      currentLocale.value = parts.length == 2
 | 
			
		||||
          ? Locale(parts[0], parts[1])
 | 
			
		||||
          : Locale(parts[0]);
 | 
			
		||||
      Get.locale = currentLocale.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final Locale locale =
 | 
			
		||||
        supportedLocales[savedLocaleKey ?? 'en_US'] ??
 | 
			
		||||
        supportedLocales['en_US']!;
 | 
			
		||||
 | 
			
		||||
    currentLocale.value = locale;
 | 
			
		||||
    Get.locale = locale;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> changeLocale(Locale newLocale) async {
 | 
			
		||||
    final String localeKey = supportedLocales.entries
 | 
			
		||||
        .firstWhere(
 | 
			
		||||
          (entry) => entry.value == newLocale,
 | 
			
		||||
          orElse: () => supportedLocales.entries.first,
 | 
			
		||||
        )
 | 
			
		||||
        .key;
 | 
			
		||||
 | 
			
		||||
    currentLocale.value = newLocale;
 | 
			
		||||
    Get.updateLocale(newLocale);
 | 
			
		||||
    await storage.setString(
 | 
			
		||||
      'locale',
 | 
			
		||||
      '${newLocale.languageCode}_${newLocale.countryCode}',
 | 
			
		||||
    );
 | 
			
		||||
    await storage.setString('locale', localeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
			
		||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
			
		||||
 | 
			
		||||
class MediaController extends GetxController {
 | 
			
		||||
  final ApiService _api = ApiService();
 | 
			
		||||
  final ApiService _api = Get.find<ApiService>();
 | 
			
		||||
  final EncryptedSharedPreferencesAsync storage =
 | 
			
		||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
			
		||||
 | 
			
		||||
@@ -22,7 +22,7 @@ class MediaController extends GetxController {
 | 
			
		||||
  RxInt typeIndex = 0.obs;
 | 
			
		||||
  RxInt modeIndex = 0.obs;
 | 
			
		||||
  RxInt random = 0.obs;
 | 
			
		||||
  Rxn<String> tag = Rxn<String>();
 | 
			
		||||
  Rxn<String> tag = Rxn<String>(null);
 | 
			
		||||
  RxBool muted = false.obs;
 | 
			
		||||
  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
			
		||||
  RxBool drawerSwipeEnabled = true.obs;
 | 
			
		||||
@@ -45,14 +45,6 @@ class MediaController extends GetxController {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<MediaItem> get filteredItems {
 | 
			
		||||
    final String typeStr = mediaTypes[typeIndex.value];
 | 
			
		||||
    return items.where((item) {
 | 
			
		||||
      final bool typeOk = typeStr == "alles" || item.mime.startsWith(typeStr);
 | 
			
		||||
      return typeOk;
 | 
			
		||||
    }).toList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Favorite>?> toggleFavorite(
 | 
			
		||||
    MediaItem item,
 | 
			
		||||
    bool isFavorite,
 | 
			
		||||
@@ -64,68 +56,82 @@ class MediaController extends GetxController {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> fetchInitial({int? id}) async {
 | 
			
		||||
  Future<Feed?> _fetchItems({int? older, int? newer}) async {
 | 
			
		||||
    if (loading.value) return null;
 | 
			
		||||
    loading.value = true;
 | 
			
		||||
    try {
 | 
			
		||||
      final Feed result = await _api.fetchItems(
 | 
			
		||||
      return await _api.fetchItems(
 | 
			
		||||
        older: older,
 | 
			
		||||
        newer: newer,
 | 
			
		||||
        type: typeIndex.value,
 | 
			
		||||
        mode: modeIndex.value,
 | 
			
		||||
        random: random.value,
 | 
			
		||||
        tag: tag.value,
 | 
			
		||||
        older: id,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      Get.snackbar(
 | 
			
		||||
        'Fehler beim Laden',
 | 
			
		||||
        'Die Daten konnten nicht abgerufen werden. Wo Internet?',
 | 
			
		||||
        snackPosition: SnackPosition.BOTTOM,
 | 
			
		||||
      );
 | 
			
		||||
      return null;
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> fetchInitial({int? id}) async {
 | 
			
		||||
    final result = await _fetchItems(older: id);
 | 
			
		||||
    if (result != null) {
 | 
			
		||||
      items.assignAll(result.items);
 | 
			
		||||
      atEnd.value = result.atEnd;
 | 
			
		||||
      atStart.value = result.atStart;
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> fetchMore() async {
 | 
			
		||||
    if (items.isEmpty || atEnd.value) return;
 | 
			
		||||
    loading.value = true;
 | 
			
		||||
    try {
 | 
			
		||||
      final Feed result = await _api.fetchItems(
 | 
			
		||||
        older: items.last.id,
 | 
			
		||||
        type: typeIndex.value,
 | 
			
		||||
        mode: modeIndex.value,
 | 
			
		||||
        random: random.value,
 | 
			
		||||
        tag: tag.value,
 | 
			
		||||
      );
 | 
			
		||||
    final result = await _fetchItems(older: items.last.id);
 | 
			
		||||
    if (result != null) {
 | 
			
		||||
      final Set<int> existingIds = items.map((e) => e.id).toSet();
 | 
			
		||||
      final List<MediaItem> newItems = result.items
 | 
			
		||||
          .where((item) => !items.any((existing) => existing.id == item.id))
 | 
			
		||||
          .where((item) => !existingIds.contains(item.id))
 | 
			
		||||
          .toList();
 | 
			
		||||
      items.addAll(newItems);
 | 
			
		||||
      items.refresh();
 | 
			
		||||
      atEnd.value = result.atEnd;
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> fetchNewer() async {
 | 
			
		||||
    if (items.isEmpty || atStart.value) return 0;
 | 
			
		||||
    loading.value = true;
 | 
			
		||||
    try {
 | 
			
		||||
      final Feed result = await _api.fetchItems(
 | 
			
		||||
        newer: items.first.id,
 | 
			
		||||
        type: typeIndex.value,
 | 
			
		||||
        mode: modeIndex.value,
 | 
			
		||||
        random: random.value,
 | 
			
		||||
        tag: tag.value,
 | 
			
		||||
      );
 | 
			
		||||
      int oldLength = filteredItems.length;
 | 
			
		||||
    final oldLength = items.length;
 | 
			
		||||
    final result = await _fetchItems(newer: items.first.id);
 | 
			
		||||
    if (result != null) {
 | 
			
		||||
      final Set<int> existingIds = items.map((e) => e.id).toSet();
 | 
			
		||||
      final List<MediaItem> newItems = result.items
 | 
			
		||||
          .where((item) => !items.any((existing) => existing.id == item.id))
 | 
			
		||||
          .where((item) => !existingIds.contains(item.id))
 | 
			
		||||
          .toList();
 | 
			
		||||
      items.insertAll(0, newItems);
 | 
			
		||||
      items.refresh();
 | 
			
		||||
      atStart.value = result.atStart;
 | 
			
		||||
      int newLength = filteredItems.length;
 | 
			
		||||
      return newLength - oldLength;
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
      return items.length - oldLength;
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> handleRefresh() async {
 | 
			
		||||
    if (loading.value) return;
 | 
			
		||||
    if (!atStart.value) {
 | 
			
		||||
      await fetchNewer();
 | 
			
		||||
    } else {
 | 
			
		||||
      await fetchInitial();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> handleLoading() async {
 | 
			
		||||
    if (!loading.value && !atEnd.value) {
 | 
			
		||||
      await fetchMore();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -178,4 +184,6 @@ class MediaController extends GetxController {
 | 
			
		||||
    await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
 | 
			
		||||
    await storage.setInt('transitionType', transitionType.value.index);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get isRandomEnabled => random.value == 1;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/themecontroller.dart';
 | 
			
		||||
@@ -14,11 +15,17 @@ import 'package:f0ckapp/screens/login.dart';
 | 
			
		||||
 | 
			
		||||
void main() async {
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
 | 
			
		||||
  await MyTranslations.loadTranslations();
 | 
			
		||||
  await AppVersion.init();
 | 
			
		||||
 | 
			
		||||
  await Future.wait([
 | 
			
		||||
    EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9'),
 | 
			
		||||
    MyTranslations.loadTranslations(),
 | 
			
		||||
    AppVersion.init(),
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  Get.put(AuthController());
 | 
			
		||||
  final MediaController mediaController = Get.put(MediaController());
 | 
			
		||||
  Get.put(ApiService());
 | 
			
		||||
  Get.put(MediaController());
 | 
			
		||||
 | 
			
		||||
  final ThemeController themeController = Get.put(ThemeController());
 | 
			
		||||
  final LocalizationController localizationController = Get.put(
 | 
			
		||||
    LocalizationController(),
 | 
			
		||||
@@ -41,7 +48,7 @@ void main() async {
 | 
			
		||||
          final Uri uri = Uri.parse(settings.name ?? '/');
 | 
			
		||||
 | 
			
		||||
          if (uri.path == '/' || uri.pathSegments.isEmpty) {
 | 
			
		||||
            return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
			
		||||
            return MaterialPageRoute(builder: (_) => const MediaGrid());
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (uri.path == '/login') {
 | 
			
		||||
@@ -49,26 +56,17 @@ void main() async {
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (uri.pathSegments.length == 1) {
 | 
			
		||||
            final int id = int.parse(uri.pathSegments.first);
 | 
			
		||||
 | 
			
		||||
            return MaterialPageRoute(
 | 
			
		||||
              builder: (_) => FutureBuilder(
 | 
			
		||||
                future: mediaController.items.isEmpty
 | 
			
		||||
                    ? mediaController.fetchInitial(id: id)
 | 
			
		||||
                    : Future.value(),
 | 
			
		||||
                builder: (context, snapshot) {
 | 
			
		||||
                  if (snapshot.connectionState == ConnectionState.done) {
 | 
			
		||||
                    return MediaDetailScreen(initialId: id);
 | 
			
		||||
                  }
 | 
			
		||||
                  return const Scaffold(
 | 
			
		||||
                    body: Center(child: CircularProgressIndicator()),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
            try {
 | 
			
		||||
              final int id = int.parse(uri.pathSegments.first);
 | 
			
		||||
              return MaterialPageRoute(
 | 
			
		||||
                builder: (_) => MediaDetailScreen(initialId: id),
 | 
			
		||||
              );
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              return MaterialPageRoute(builder: (_) => const MediaGrid());
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
			
		||||
          return MaterialPageRoute(builder: (_) => const MediaGrid());
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
			
		||||
                          const Icon(Icons.error),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                : SizedBox.expand(
 | 
			
		||||
                : Center(
 | 
			
		||||
                    child: VideoWidget(
 | 
			
		||||
                      details: widget.item,
 | 
			
		||||
                      isActive: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,19 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:share_plus/share_plus.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/actiontag.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/favorite_avatars.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/favoritesection.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/fullscreen.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
 | 
			
		||||
enum ShareAction { media, directLink, postLink }
 | 
			
		||||
 | 
			
		||||
class MediaDetailScreen extends StatefulWidget {
 | 
			
		||||
  final int initialId;
 | 
			
		||||
  const MediaDetailScreen({super.key, required this.initialId});
 | 
			
		||||
@@ -26,19 +29,70 @@ class MediaDetailScreen extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  late PageController _pageController;
 | 
			
		||||
  PageController? _pageController;
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
  final AuthController authController = Get.find<AuthController>();
 | 
			
		||||
  int? _currentIndex;
 | 
			
		||||
  final _currentIndex = 0.obs;
 | 
			
		||||
  final _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
			
		||||
 | 
			
		||||
  bool _isLoading = true;
 | 
			
		||||
  bool _itemNotFound = false;
 | 
			
		||||
  final Set<int> _readyItemIds = {};
 | 
			
		||||
 | 
			
		||||
  final List<PopupMenuEntry<ShareAction>> _shareMenuItems = const [
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.media,
 | 
			
		||||
      child: ListTile(leading: Icon(Icons.image), title: Text('Als Datei')),
 | 
			
		||||
    ),
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.directLink,
 | 
			
		||||
      child: ListTile(leading: Icon(Icons.link), title: Text('Link zur Datei')),
 | 
			
		||||
    ),
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.postLink,
 | 
			
		||||
      child: ListTile(
 | 
			
		||||
        leading: Icon(Icons.article),
 | 
			
		||||
        title: Text('Link zum f0ck'),
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    final int idx = mediaController.items.indexWhere(
 | 
			
		||||
    _loadInitialItem();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _loadInitialItem() async {
 | 
			
		||||
    int initialIndex = mediaController.items.indexWhere(
 | 
			
		||||
      (item) => item.id == widget.initialId,
 | 
			
		||||
    );
 | 
			
		||||
    _currentIndex = idx >= 0 ? idx : 0;
 | 
			
		||||
    _pageController = PageController(initialPage: _currentIndex!);
 | 
			
		||||
 | 
			
		||||
    if (initialIndex < 0) {
 | 
			
		||||
      await mediaController.fetchInitial(id: widget.initialId + 20);
 | 
			
		||||
      initialIndex = mediaController.items.indexWhere(
 | 
			
		||||
        (item) => item.id == widget.initialId,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (initialIndex < 0) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          _itemNotFound = true;
 | 
			
		||||
          _isLoading = false;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mounted) {
 | 
			
		||||
      _currentIndex.value = initialIndex;
 | 
			
		||||
      _pageController = PageController(initialPage: initialIndex);
 | 
			
		||||
      if (mediaController.items[initialIndex].mime.startsWith('image/')) {
 | 
			
		||||
        _readyItemIds.add(mediaController.items[initialIndex].id);
 | 
			
		||||
      }
 | 
			
		||||
      setState(() => _isLoading = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showMsg(String message) {
 | 
			
		||||
@@ -49,15 +103,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onPageChanged(int idx) {
 | 
			
		||||
    if (idx != _currentIndex) {
 | 
			
		||||
      setState(() => _currentIndex = idx);
 | 
			
		||||
    if (idx != _currentIndex.value) {
 | 
			
		||||
      _currentIndex.value = idx;
 | 
			
		||||
      final item = mediaController.items[idx];
 | 
			
		||||
      if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
 | 
			
		||||
        setState(() => _readyItemIds.add(item.id));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (idx >= mediaController.items.length - 2 &&
 | 
			
		||||
        !mediaController.loading.value &&
 | 
			
		||||
        !mediaController.atEnd.value) {
 | 
			
		||||
      mediaController.fetchMore();
 | 
			
		||||
    }
 | 
			
		||||
    if (idx <= 1 &&
 | 
			
		||||
    } else if (idx <= 1 &&
 | 
			
		||||
        !mediaController.loading.value &&
 | 
			
		||||
        !mediaController.atStart.value) {
 | 
			
		||||
      mediaController.fetchNewer();
 | 
			
		||||
@@ -65,22 +122,58 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _downloadMedia(MediaItem item) async {
 | 
			
		||||
    final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
 | 
			
		||||
    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
			
		||||
    try {
 | 
			
		||||
      final File file = await DefaultCacheManager().getSingleFile(
 | 
			
		||||
        item.mediaUrl,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
			
		||||
      'filePath': file.path,
 | 
			
		||||
      'fileName': item.dest,
 | 
			
		||||
    });
 | 
			
		||||
      final bool? success = await _mediaSaverChannel.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.');
 | 
			
		||||
      success == true
 | 
			
		||||
          ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
			
		||||
          : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showMsg('Fehler beim Download: $e');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _handleShareAction(ShareAction value, MediaItem item) async {
 | 
			
		||||
    try {
 | 
			
		||||
      if (value == ShareAction.media) {
 | 
			
		||||
        final File file = await DefaultCacheManager().getSingleFile(
 | 
			
		||||
          item.mediaUrl,
 | 
			
		||||
        );
 | 
			
		||||
        final Uint8List bytes = await file.readAsBytes();
 | 
			
		||||
        final params = ShareParams(
 | 
			
		||||
          files: [XFile.fromData(bytes, mimeType: item.mime)],
 | 
			
		||||
        );
 | 
			
		||||
        await SharePlus.instance.share(params);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      final String textToShare;
 | 
			
		||||
      switch (value) {
 | 
			
		||||
        case ShareAction.directLink:
 | 
			
		||||
          textToShare = item.mediaUrl;
 | 
			
		||||
          break;
 | 
			
		||||
        case ShareAction.postLink:
 | 
			
		||||
          textToShare = item.postUrl;
 | 
			
		||||
          break;
 | 
			
		||||
        case ShareAction.media:
 | 
			
		||||
          return;
 | 
			
		||||
      }
 | 
			
		||||
      await SharePlus.instance.share(ShareParams(text: textToShare));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showMsg('Fehler beim Teilen: $e');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _pageController.dispose();
 | 
			
		||||
    _pageController?.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +186,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    } else if (item.mime.startsWith('video/') ||
 | 
			
		||||
        item.mime.startsWith('audio/')) {
 | 
			
		||||
      return VideoWidget(details: item, isActive: isActive);
 | 
			
		||||
      return VideoWidget(
 | 
			
		||||
        details: item,
 | 
			
		||||
        isActive: isActive,
 | 
			
		||||
        onInitialized: () {
 | 
			
		||||
          if (mounted && !_readyItemIds.contains(item.id)) {
 | 
			
		||||
            setState(() => _readyItemIds.add(item.id));
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return const Icon(Icons.help_outline, size: 100);
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,209 +202,134 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (_isLoading) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(title: Text('Lade f0ck #${widget.initialId}...')),
 | 
			
		||||
        body: const Center(child: CircularProgressIndicator()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (_itemNotFound) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(title: const Text('Fehler')),
 | 
			
		||||
        body: const Center(child: Text('f0ck nicht gefunden.')),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Obx(
 | 
			
		||||
      () => PageView.builder(
 | 
			
		||||
        controller: _pageController,
 | 
			
		||||
        controller: _pageController!,
 | 
			
		||||
        itemCount: mediaController.items.length,
 | 
			
		||||
        onPageChanged: _onPageChanged,
 | 
			
		||||
        itemBuilder: (context, index) {
 | 
			
		||||
          final MediaItem item = mediaController.items[index];
 | 
			
		||||
          final bool isActive = index == _currentIndex;
 | 
			
		||||
          final bool isFavorite =
 | 
			
		||||
              item.favorites?.any(
 | 
			
		||||
                (f) => f.userId == authController.userId.value,
 | 
			
		||||
              ) ??
 | 
			
		||||
              false;
 | 
			
		||||
          final bool isReady = _readyItemIds.contains(item.id);
 | 
			
		||||
 | 
			
		||||
          return Scaffold(
 | 
			
		||||
            endDrawer: EndDrawer(),
 | 
			
		||||
            endDrawerEnableOpenDragGesture:
 | 
			
		||||
                mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
            body: CustomScrollView(
 | 
			
		||||
              slivers: [
 | 
			
		||||
                SliverAppBar(
 | 
			
		||||
                  floating: false,
 | 
			
		||||
                  pinned: true,
 | 
			
		||||
                  title: Text('f0ck #${item.id}'),
 | 
			
		||||
                  actions: [
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.fullscreen),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Get.to(
 | 
			
		||||
                          FullScreenMediaView(item: item),
 | 
			
		||||
                          fullscreenDialog: true,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.download),
 | 
			
		||||
                      onPressed: () async {
 | 
			
		||||
                        await _downloadMedia(item);
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    PopupMenuButton<String>(
 | 
			
		||||
                      onSelected: (value) async {
 | 
			
		||||
                        switch (value) {
 | 
			
		||||
                          case 'media':
 | 
			
		||||
                            File file = await DefaultCacheManager()
 | 
			
		||||
                                .getSingleFile(item.mediaUrl);
 | 
			
		||||
                            Uint8List bytes = await file.readAsBytes();
 | 
			
		||||
                            final params = ShareParams(
 | 
			
		||||
                              files: [
 | 
			
		||||
                                XFile.fromData(bytes, mimeType: item.mime),
 | 
			
		||||
                              ],
 | 
			
		||||
                            );
 | 
			
		||||
                            await SharePlus.instance.share(params);
 | 
			
		||||
                            break;
 | 
			
		||||
                          case 'direct_link':
 | 
			
		||||
                            await SharePlus.instance.share(
 | 
			
		||||
                              ShareParams(text: item.mediaUrl),
 | 
			
		||||
                            );
 | 
			
		||||
                            break;
 | 
			
		||||
                          case 'post_link':
 | 
			
		||||
                            await SharePlus.instance.share(
 | 
			
		||||
                              ShareParams(text: item.postUrl),
 | 
			
		||||
                            );
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                      itemBuilder: (context) => [
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'media',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.image),
 | 
			
		||||
                            title: const Text('Als Datei'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'direct_link',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.link),
 | 
			
		||||
                            title: const Text('Link zur Datei'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'post_link',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.article),
 | 
			
		||||
                            title: const Text('Link zum f0ck'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                      icon: const Icon(Icons.share),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Builder(
 | 
			
		||||
                      builder: (context) => IconButton(
 | 
			
		||||
                        icon: const Icon(Icons.menu),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
            appBar: AppBar(
 | 
			
		||||
              title: Text('f0ck #${item.id}'),
 | 
			
		||||
              actions: [
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.fullscreen),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    Get.to(
 | 
			
		||||
                      FullScreenMediaView(item: item),
 | 
			
		||||
                      fullscreenDialog: true,
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                SliverToBoxAdapter(
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      AnimatedBuilder(
 | 
			
		||||
                        animation: _pageController,
 | 
			
		||||
                        builder: (context, child) {
 | 
			
		||||
                          return buildAnimatedTransition(
 | 
			
		||||
                            context: context,
 | 
			
		||||
                            pageController: _pageController,
 | 
			
		||||
                            index: index,
 | 
			
		||||
                            controller: mediaController,
 | 
			
		||||
                            child: child!,
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                        child: _buildMedia(item, isActive),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const SizedBox(height: 16),
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
			
		||||
                        child: Column(
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                ...item.tags?.map(
 | 
			
		||||
                                  (tag) => Padding(
 | 
			
		||||
                                    padding: const EdgeInsets.only(right: 6),
 | 
			
		||||
                                    child: ActionTag(tag, (onTagTap) {
 | 
			
		||||
                                      if (tag.tag == 'sfw' || tag.tag == 'nsfw') {
 | 
			
		||||
                                        return;
 | 
			
		||||
                                      }
 | 
			
		||||
                                      mediaController.setTag(onTagTap);
 | 
			
		||||
                                      Get.offAllNamed('/');
 | 
			
		||||
                                    }),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ) ?? [],
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            if (authController.isLoggedIn) ...[
 | 
			
		||||
                              const SizedBox(height: 20),
 | 
			
		||||
                              Row(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: SingleChildScrollView(
 | 
			
		||||
                                      scrollDirection: Axis.horizontal,
 | 
			
		||||
                                      child: FavoriteAvatars(
 | 
			
		||||
                                        favorites: item.favorites ?? [],
 | 
			
		||||
                                        brightness: Theme.of(
 | 
			
		||||
                                          context,
 | 
			
		||||
                                        ).brightness,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  IconButton(
 | 
			
		||||
                                    icon: isFavorite
 | 
			
		||||
                                        ? const Icon(Icons.favorite)
 | 
			
		||||
                                        : const Icon(Icons.favorite_outline),
 | 
			
		||||
                                    color: Colors.red,
 | 
			
		||||
                                    onPressed: () async {
 | 
			
		||||
                                      final List<Favorite>? newFavorites =
 | 
			
		||||
                                          await mediaController.toggleFavorite(
 | 
			
		||||
                                            item,
 | 
			
		||||
                                            isFavorite,
 | 
			
		||||
                                          );
 | 
			
		||||
                                      if (newFavorites != null) {
 | 
			
		||||
                                        mediaController.items[index] = item
 | 
			
		||||
                                            .copyWith(favorites: newFavorites);
 | 
			
		||||
                                        mediaController.items.refresh();
 | 
			
		||||
                                      }
 | 
			
		||||
                                      setState(() {});
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.download),
 | 
			
		||||
                  onPressed: () async {
 | 
			
		||||
                    await _downloadMedia(item);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                PopupMenuButton<ShareAction>(
 | 
			
		||||
                  onSelected: (value) => _handleShareAction(value, item),
 | 
			
		||||
                  itemBuilder: (context) => _shareMenuItems,
 | 
			
		||||
                  icon: const Icon(Icons.share),
 | 
			
		||||
                ),
 | 
			
		||||
                Builder(
 | 
			
		||||
                  builder: (context) => IconButton(
 | 
			
		||||
                    icon: const Icon(Icons.menu),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            persistentFooterButtons: [
 | 
			
		||||
              Obx(() {
 | 
			
		||||
                if (mediaController.tag.value != null) {
 | 
			
		||||
                  return Center(
 | 
			
		||||
                    child: InputChip(
 | 
			
		||||
                      label: Text(mediaController.tag.value!),
 | 
			
		||||
                      onDeleted: () {
 | 
			
		||||
                        mediaController.setTag(null);
 | 
			
		||||
                        Get.offAllNamed('/');
 | 
			
		||||
                      },
 | 
			
		||||
            body: SingleChildScrollView(
 | 
			
		||||
              child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                children: [
 | 
			
		||||
                  AnimatedBuilder(
 | 
			
		||||
                    animation: _pageController!,
 | 
			
		||||
                    builder: (context, child) {
 | 
			
		||||
                      return buildAnimatedTransition(
 | 
			
		||||
                        context: context,
 | 
			
		||||
                        pageController: _pageController!,
 | 
			
		||||
                        index: index,
 | 
			
		||||
                        controller: mediaController,
 | 
			
		||||
                        child: child!,
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    child: Obx(
 | 
			
		||||
                      () => _buildMedia(item, index == _currentIndex.value),
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                } else {
 | 
			
		||||
                  return SizedBox.shrink();
 | 
			
		||||
                }
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  if (isReady)
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Wrap(
 | 
			
		||||
                            spacing: 6.0,
 | 
			
		||||
                            runSpacing: 4.0,
 | 
			
		||||
                            alignment: WrapAlignment.center,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              ...item.tags?.map(
 | 
			
		||||
                                    (tag) => ActionTag(
 | 
			
		||||
                                      tag,
 | 
			
		||||
                                      (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
			
		||||
                                          ? (onTagTap) => {}
 | 
			
		||||
                                          : (onTagTap) {
 | 
			
		||||
                                              mediaController.setTag(onTagTap);
 | 
			
		||||
                                              Get.offAllNamed('/');
 | 
			
		||||
                                            },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ) ??
 | 
			
		||||
                                  [],
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                          Obx(
 | 
			
		||||
                            () => Visibility(
 | 
			
		||||
                              visible: authController.isLoggedIn,
 | 
			
		||||
                              child: Padding(
 | 
			
		||||
                                padding: const EdgeInsets.only(top: 20.0),
 | 
			
		||||
                                child: FavoriteSection(
 | 
			
		||||
                                  item: item,
 | 
			
		||||
                                  index: index,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                  else
 | 
			
		||||
                    const SizedBox.shrink(),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            persistentFooterButtons: mediaController.tag.value != null
 | 
			
		||||
                ? [TagFooter()]
 | 
			
		||||
                : null,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:pullex/pullex.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/mediadetail.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/media_tile.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
@@ -24,151 +24,162 @@ class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
    initialRefresh: false,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  late final _MediaGridAppBar _appBar;
 | 
			
		||||
  late final _MediaGridBody _body;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _mediaController.fetchInitial();
 | 
			
		||||
    _appBar = _MediaGridAppBar(mediaController: _mediaController);
 | 
			
		||||
    _body = _MediaGridBody(
 | 
			
		||||
      refreshController: _refreshController,
 | 
			
		||||
      mediaController: _mediaController,
 | 
			
		||||
      scrollController: _scrollController,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _scrollController.dispose();
 | 
			
		||||
    _refreshController.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      endDrawer: EndDrawer(),
 | 
			
		||||
      endDrawerEnableOpenDragGesture: _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
      body: PullexRefresh(
 | 
			
		||||
        controller: _refreshController,
 | 
			
		||||
        enablePullDown: true,
 | 
			
		||||
        enablePullUp: true,
 | 
			
		||||
        header: MaterialHeader(offset: 140),
 | 
			
		||||
        onRefresh: () async {
 | 
			
		||||
          try {
 | 
			
		||||
            if (_mediaController.loading.value) return;
 | 
			
		||||
            if (!_mediaController.atStart.value) {
 | 
			
		||||
              await _mediaController.fetchNewer();
 | 
			
		||||
            } else {
 | 
			
		||||
              await _mediaController.fetchInitial();
 | 
			
		||||
            }
 | 
			
		||||
          } finally {
 | 
			
		||||
            _refreshController.refreshCompleted();
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onLoading: () async {
 | 
			
		||||
          try {
 | 
			
		||||
            if (!_mediaController.loading.value &&
 | 
			
		||||
                !_mediaController.atEnd.value) {
 | 
			
		||||
              await _mediaController.fetchMore();
 | 
			
		||||
            }
 | 
			
		||||
          } finally {
 | 
			
		||||
            _refreshController.loadComplete();
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        child: CustomScrollView(
 | 
			
		||||
          controller: _scrollController,
 | 
			
		||||
          slivers: [
 | 
			
		||||
            SliverAppBar(
 | 
			
		||||
              pinned: false,
 | 
			
		||||
              snap: true,
 | 
			
		||||
              floating: true,
 | 
			
		||||
              title: GestureDetector(
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Image.asset(
 | 
			
		||||
                      'assets/images/f0ck_small.webp',
 | 
			
		||||
                      fit: BoxFit.fitHeight,
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(width: 10),
 | 
			
		||||
                    const Text('fApp', style: TextStyle(fontSize: 24)),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              actions: [
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.search),
 | 
			
		||||
                  onPressed: () async {
 | 
			
		||||
                    await showSearch(
 | 
			
		||||
                      context: context,
 | 
			
		||||
                      delegate: CustomSearchDelegate(),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                Obx(
 | 
			
		||||
                  () => IconButton(
 | 
			
		||||
                    icon: Icon(
 | 
			
		||||
                      _mediaController.random.value == 1
 | 
			
		||||
                          ? Icons.shuffle_on_outlined
 | 
			
		||||
                          : Icons.shuffle,
 | 
			
		||||
                    ),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      _mediaController.toggleRandom();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Builder(
 | 
			
		||||
                  builder: (context) {
 | 
			
		||||
                    return IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.menu),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Obx(
 | 
			
		||||
              () => SliverPadding(
 | 
			
		||||
                padding: const EdgeInsets.all(4),
 | 
			
		||||
                sliver: SliverGrid(
 | 
			
		||||
                  delegate: SliverChildBuilderDelegate((context, index) {
 | 
			
		||||
                    final item = _mediaController.filteredItems[index];
 | 
			
		||||
                    return GestureDetector(
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        final item = _mediaController.filteredItems[index];
 | 
			
		||||
                        Get.to(() => MediaDetailScreen(initialId: item.id));
 | 
			
		||||
                      },
 | 
			
		||||
                      child: MediaTile(item: item),
 | 
			
		||||
                    );
 | 
			
		||||
                  }, childCount: _mediaController.filteredItems.length),
 | 
			
		||||
                  gridDelegate: _mediaController.crossAxisCount.value == 0
 | 
			
		||||
                      ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                          maxCrossAxisExtent: 150,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
                          mainAxisSpacing: 5,
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
                        )
 | 
			
		||||
                      : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                          crossAxisCount: _mediaController.crossAxisCount.value,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
                          mainAxisSpacing: 5,
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
                        ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
    return Obx(
 | 
			
		||||
      () => Scaffold(
 | 
			
		||||
        endDrawer: const EndDrawer(),
 | 
			
		||||
        endDrawerEnableOpenDragGesture:
 | 
			
		||||
            _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
        bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
        appBar: _appBar,
 | 
			
		||||
        body: _body,
 | 
			
		||||
        persistentFooterButtons: _mediaController.tag.value != null
 | 
			
		||||
            ? [TagFooter()]
 | 
			
		||||
            : null,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget {
 | 
			
		||||
  const _MediaGridAppBar({required this.mediaController});
 | 
			
		||||
 | 
			
		||||
  final MediaController mediaController;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppBar(
 | 
			
		||||
      title: InkWell(
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          mediaController.setTag(null);
 | 
			
		||||
        },
 | 
			
		||||
        child: Row(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          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(
 | 
			
		||||
              mediaController.isRandomEnabled
 | 
			
		||||
                  ? Icons.shuffle_on_outlined
 | 
			
		||||
                  : Icons.shuffle,
 | 
			
		||||
            ),
 | 
			
		||||
            onPressed: mediaController.toggleRandom,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          icon: const Icon(Icons.menu),
 | 
			
		||||
          onPressed: () => Scaffold.of(context).openEndDrawer(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
  const _MediaGridBody({
 | 
			
		||||
    required this.refreshController,
 | 
			
		||||
    required this.mediaController,
 | 
			
		||||
    required this.scrollController,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final PullexRefreshController refreshController;
 | 
			
		||||
  final MediaController mediaController;
 | 
			
		||||
  final ScrollController scrollController;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return PullexRefresh(
 | 
			
		||||
      controller: refreshController,
 | 
			
		||||
      enablePullDown: true,
 | 
			
		||||
      enablePullUp: true,
 | 
			
		||||
      header: const MaterialHeader(),
 | 
			
		||||
      onRefresh: () async {
 | 
			
		||||
        try {
 | 
			
		||||
          await mediaController.handleRefresh();
 | 
			
		||||
        } finally {
 | 
			
		||||
          refreshController.refreshCompleted();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLoading: () async {
 | 
			
		||||
        try {
 | 
			
		||||
          await mediaController.handleLoading();
 | 
			
		||||
        } finally {
 | 
			
		||||
          refreshController.loadComplete();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      child: Obx(
 | 
			
		||||
        () => GridView.builder(
 | 
			
		||||
          addAutomaticKeepAlives: false,
 | 
			
		||||
          controller: scrollController,
 | 
			
		||||
          physics: const NeverScrollableScrollPhysics(),
 | 
			
		||||
          shrinkWrap: true,
 | 
			
		||||
          padding: const EdgeInsets.all(4),
 | 
			
		||||
          itemCount: mediaController.items.length,
 | 
			
		||||
          gridDelegate: mediaController.crossAxisCount.value == 0
 | 
			
		||||
              ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                  maxCrossAxisExtent: 150,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
                  mainAxisSpacing: 5,
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
                )
 | 
			
		||||
              : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                  crossAxisCount: mediaController.crossAxisCount.value,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
                  mainAxisSpacing: 5,
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
                ),
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
            final item = mediaController.items[index];
 | 
			
		||||
            return GestureDetector(
 | 
			
		||||
              key: ValueKey(item.id),
 | 
			
		||||
              onTap: () => Get.toNamed('/${item.id}'),
 | 
			
		||||
              child: MediaTile(item: item),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      persistentFooterButtons: [
 | 
			
		||||
        Obx(() {
 | 
			
		||||
          if (_mediaController.tag.value != null) {
 | 
			
		||||
            return Center(
 | 
			
		||||
              child: InputChip(
 | 
			
		||||
                label: Text(_mediaController.tag.value!),
 | 
			
		||||
                onDeleted: () {
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                  Get.offAllNamed('/');
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            return SizedBox.shrink();
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,6 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    onChanged: (int? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setCrossAxisCount(newValue);
 | 
			
		||||
                        setState(() {});
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -93,7 +92,6 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    onChanged: (PageTransition? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setTransitionType(newValue);
 | 
			
		||||
                        setState(() {});
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,9 @@ class ApiService extends GetConnect {
 | 
			
		||||
      feed.items.sort((a, b) => b.id.compareTo(a.id));
 | 
			
		||||
      return feed;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (Get.isSnackbarOpen == false) {
 | 
			
		||||
        Get.snackbar('Fehler', 'Fehler beim Laden der Items');
 | 
			
		||||
      }
 | 
			
		||||
      throw Exception('Fehler beim Laden der Items');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class SmartRefreshIndicator extends StatelessWidget {
 | 
			
		||||
  final Future<void> Function() onRefresh;
 | 
			
		||||
  final Widget child;
 | 
			
		||||
 | 
			
		||||
  const SmartRefreshIndicator({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.onRefresh,
 | 
			
		||||
    required this.child,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(context) {
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
      builder: (context, constraints) => RefreshIndicator(
 | 
			
		||||
        onRefresh: onRefresh,
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          physics: const AlwaysScrollableScrollPhysics(),
 | 
			
		||||
          child: ConstrainedBox(
 | 
			
		||||
            constraints: BoxConstraints(minHeight: constraints.maxHeight),
 | 
			
		||||
            child: child,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
 | 
			
		||||
class FavoriteAvatars extends StatelessWidget {
 | 
			
		||||
  final List favorites;
 | 
			
		||||
  final Brightness brightness;
 | 
			
		||||
 | 
			
		||||
  const FavoriteAvatars({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.favorites,
 | 
			
		||||
    required this.brightness,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Row(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
        ...favorites.map((favorite) {
 | 
			
		||||
          return Container(
 | 
			
		||||
            height: 32,
 | 
			
		||||
            width: 32,
 | 
			
		||||
            margin: const EdgeInsets.only(right: 5.0),
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              border: Border.all(
 | 
			
		||||
                color: brightness == Brightness.dark
 | 
			
		||||
                    ? Colors.white
 | 
			
		||||
                    : Colors.black,
 | 
			
		||||
                width: 1.0,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            child: CachedNetworkImage(
 | 
			
		||||
              imageUrl: favorite.avatarUrl,
 | 
			
		||||
              fit: BoxFit.cover,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								lib/widgets/favoritesection.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								lib/widgets/favoritesection.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
 | 
			
		||||
class FavoriteSection extends StatelessWidget {
 | 
			
		||||
  final MediaItem item;
 | 
			
		||||
  final int index;
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
  final AuthController authController = Get.find<AuthController>();
 | 
			
		||||
 | 
			
		||||
  FavoriteSection({super.key, required this.item, required this.index});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final bool isFavorite =
 | 
			
		||||
        item.favorites?.any((f) => f.userId == authController.userId.value) ??
 | 
			
		||||
        false;
 | 
			
		||||
 | 
			
		||||
    return Row(
 | 
			
		||||
      children: [
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: SingleChildScrollView(
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            child: Row(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
              children: [
 | 
			
		||||
                ...(item.favorites ?? []).map((favorite) {
 | 
			
		||||
                  return Container(
 | 
			
		||||
                    height: 32,
 | 
			
		||||
                    width: 32,
 | 
			
		||||
                    margin: const EdgeInsets.only(right: 5.0),
 | 
			
		||||
                    decoration: BoxDecoration(
 | 
			
		||||
                      border: Border.all(
 | 
			
		||||
                        color: Theme.of(context).brightness == Brightness.dark
 | 
			
		||||
                            ? Colors.white
 | 
			
		||||
                            : Colors.black,
 | 
			
		||||
                        width: 1.0,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: CachedNetworkImage(
 | 
			
		||||
                      imageUrl: favorite.avatarUrl,
 | 
			
		||||
                      fit: BoxFit.cover,
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                }),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          icon: isFavorite
 | 
			
		||||
              ? const Icon(Icons.favorite)
 | 
			
		||||
              : const Icon(Icons.favorite_outline),
 | 
			
		||||
          color: Colors.red,
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            final List<Favorite>? newFavorites = await mediaController
 | 
			
		||||
                .toggleFavorite(item, isFavorite);
 | 
			
		||||
            if (newFavorites != null) {
 | 
			
		||||
              mediaController.items[index] = item.copyWith(
 | 
			
		||||
                favorites: newFavorites,
 | 
			
		||||
              );
 | 
			
		||||
              mediaController.items.refresh();
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
 | 
			
		||||
@@ -13,9 +12,6 @@ class MediaTile extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return RepaintBoundary(
 | 
			
		||||
      child: InkWell(
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          Get.toNamed('/${item.id}');
 | 
			
		||||
        },
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          fit: StackFit.expand,
 | 
			
		||||
          children: [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								lib/widgets/tagfooter.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/widgets/tagfooter.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class TagFooter extends StatelessWidget {
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
 | 
			
		||||
  TagFooter({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Obx(() {
 | 
			
		||||
      if (mediaController.tag.value != null) {
 | 
			
		||||
        return Center(
 | 
			
		||||
          child: InputChip(
 | 
			
		||||
            label: Text(mediaController.tag.value!),
 | 
			
		||||
            onDeleted: () {
 | 
			
		||||
              mediaController.setTag(null);
 | 
			
		||||
              Get.offAllNamed('/');
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return const SizedBox.shrink();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,12 +14,14 @@ class VideoWidget extends StatefulWidget {
 | 
			
		||||
  final MediaItem details;
 | 
			
		||||
  final bool isActive;
 | 
			
		||||
  final bool fullScreen;
 | 
			
		||||
  final VoidCallback? onInitialized;
 | 
			
		||||
 | 
			
		||||
  const VideoWidget({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.details,
 | 
			
		||||
    required this.isActive,
 | 
			
		||||
    this.fullScreen = false,
 | 
			
		||||
    this.onInitialized,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -29,6 +31,7 @@ class VideoWidget extends StatefulWidget {
 | 
			
		||||
class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  final MediaController controller = Get.find<MediaController>();
 | 
			
		||||
  late CachedVideoPlayerPlusController _controller;
 | 
			
		||||
  late Worker _muteWorker;
 | 
			
		||||
  bool _showControls = false;
 | 
			
		||||
  Timer? _hideControlsTimer;
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +39,11 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _initController();
 | 
			
		||||
    _muteWorker = ever(controller.muted, (bool muted) {
 | 
			
		||||
      if (_controller.value.isInitialized) {
 | 
			
		||||
        _controller.setVolume(muted ? 0.0 : 1.0);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _initController() async {
 | 
			
		||||
@@ -43,6 +51,8 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
      Uri.parse(widget.details.mediaUrl),
 | 
			
		||||
    );
 | 
			
		||||
    await _controller.initialize();
 | 
			
		||||
    widget.onInitialized?.call();
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    setState(() {});
 | 
			
		||||
    _controller.addListener(() => setState(() {}));
 | 
			
		||||
    _controller.setLooping(true);
 | 
			
		||||
@@ -67,6 +77,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _muteWorker.dispose();
 | 
			
		||||
    _controller.dispose();
 | 
			
		||||
    _hideControlsTimer?.cancel();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
@@ -87,11 +98,6 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final bool muted = controller.muted.value;
 | 
			
		||||
    if (_controller.value.isInitialized &&
 | 
			
		||||
        _controller.value.volume != (muted ? 0.0 : 1.0)) {
 | 
			
		||||
      _controller.setVolume(muted ? 0.0 : 1.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
			
		||||
 | 
			
		||||
    Widget mediaContent;
 | 
			
		||||
@@ -131,7 +137,6 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
                    muted: muted,
 | 
			
		||||
                    onMuteToggle: () {
 | 
			
		||||
                      controller.toggleMuted();
 | 
			
		||||
                      setState(() {});
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user