This commit is contained in:
		@@ -1,18 +1,14 @@
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/feed.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
 | 
			
		||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
			
		||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
			
		||||
 | 
			
		||||
class MediaController extends GetxController {
 | 
			
		||||
  final ApiService _api = Get.find<ApiService>();
 | 
			
		||||
  final EncryptedSharedPreferencesAsync storage =
 | 
			
		||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
			
		||||
 | 
			
		||||
  RxList<MediaItem> items = <MediaItem>[].obs;
 | 
			
		||||
  RxBool loading = false.obs;
 | 
			
		||||
@@ -23,12 +19,6 @@ class MediaController extends GetxController {
 | 
			
		||||
  RxInt modeIndex = 0.obs;
 | 
			
		||||
  RxInt random = 0.obs;
 | 
			
		||||
  Rxn<String> tag = Rxn<String>(null);
 | 
			
		||||
  RxBool muted = false.obs;
 | 
			
		||||
  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
			
		||||
  RxBool drawerSwipeEnabled = true.obs;
 | 
			
		||||
  RxInt crossAxisCount = 0.obs;
 | 
			
		||||
  RxInt videoControlsTimerNotifier = 0.obs;
 | 
			
		||||
  RxInt hideControlsNotifier = 0.obs;
 | 
			
		||||
 | 
			
		||||
  void setTypeIndex(int idx) {
 | 
			
		||||
    typeIndex.value = idx;
 | 
			
		||||
@@ -137,58 +127,10 @@ class MediaController extends GetxController {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void toggleMuted() {
 | 
			
		||||
    muted.value = !muted.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setMuted(bool value) {
 | 
			
		||||
    muted.value = value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setTransitionType(PageTransition type) async {
 | 
			
		||||
    transitionType.value = type;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setCrossAxisCount(int value) async {
 | 
			
		||||
    crossAxisCount.value = value;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setDrawerSwipeEnabled(bool enabled) async {
 | 
			
		||||
    drawerSwipeEnabled.value = enabled;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void toggleRandom() {
 | 
			
		||||
    random.value = random.value == 1 ? 0 : 1;
 | 
			
		||||
    fetchInitial();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void resetVideoControlsTimer() => videoControlsTimerNotifier.value++;
 | 
			
		||||
  void hideVideoControls() => hideControlsNotifier.value++;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onInit() async {
 | 
			
		||||
    super.onInit();
 | 
			
		||||
    await loadSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> loadSettings() async {
 | 
			
		||||
    muted.value = await storage.getBoolean('muted') ?? false;
 | 
			
		||||
    crossAxisCount.value = await storage.getInt('crossAxisCount') ?? 0;
 | 
			
		||||
    drawerSwipeEnabled.value =
 | 
			
		||||
        await storage.getBoolean('drawerSwipeEnabled') ?? true;
 | 
			
		||||
    transitionType.value =
 | 
			
		||||
        PageTransition.values[await storage.getInt('transitionType') ?? 0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> saveSettings() async {
 | 
			
		||||
    await storage.setBoolean('muted', muted.value);
 | 
			
		||||
    await storage.setInt('crossAxisCount', crossAxisCount.value);
 | 
			
		||||
    await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
 | 
			
		||||
    await storage.setInt('transitionType', transitionType.value.index);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get isRandomEnabled => random.value == 1;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								lib/controller/settingscontroller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								lib/controller/settingscontroller.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
 | 
			
		||||
class _StorageKeys {
 | 
			
		||||
  static const String muted = 'muted';
 | 
			
		||||
  static const String crossAxisCount = 'crossAxisCount';
 | 
			
		||||
  static const String drawerSwipeEnabled = 'drawerSwipeEnabled';
 | 
			
		||||
  static const String transitionType = 'transitionType';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SettingsController extends GetxController {
 | 
			
		||||
  final EncryptedSharedPreferencesAsync storage =
 | 
			
		||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
			
		||||
 | 
			
		||||
  RxBool muted = false.obs;
 | 
			
		||||
  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
			
		||||
  RxBool drawerSwipeEnabled = true.obs;
 | 
			
		||||
  RxInt crossAxisCount = 0.obs;
 | 
			
		||||
 | 
			
		||||
  RxInt videoControlsTimerNotifier = 0.obs;
 | 
			
		||||
  RxInt hideControlsNotifier = 0.obs;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onInit() {
 | 
			
		||||
    super.onInit();
 | 
			
		||||
    loadSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void toggleMuted() {
 | 
			
		||||
    muted.value = !muted.value;
 | 
			
		||||
    saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setMuted(bool value) {
 | 
			
		||||
    muted.value = value;
 | 
			
		||||
    saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setTransitionType(PageTransition type) async {
 | 
			
		||||
    transitionType.value = type;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setCrossAxisCount(int value) async {
 | 
			
		||||
    crossAxisCount.value = value;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setDrawerSwipeEnabled(bool enabled) async {
 | 
			
		||||
    drawerSwipeEnabled.value = enabled;
 | 
			
		||||
    await saveSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void resetVideoControlsTimer() => videoControlsTimerNotifier.value++;
 | 
			
		||||
  void hideVideoControls() => hideControlsNotifier.value++;
 | 
			
		||||
 | 
			
		||||
  Future<void> loadSettings() async {
 | 
			
		||||
    muted.value = await storage.getBoolean(_StorageKeys.muted) ?? false;
 | 
			
		||||
    crossAxisCount.value =
 | 
			
		||||
        await storage.getInt(_StorageKeys.crossAxisCount) ?? 0;
 | 
			
		||||
    drawerSwipeEnabled.value =
 | 
			
		||||
        await storage.getBoolean(_StorageKeys.drawerSwipeEnabled) ?? true;
 | 
			
		||||
    transitionType.value = PageTransition
 | 
			
		||||
        .values[await storage.getInt(_StorageKeys.transitionType) ?? 0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> saveSettings() async {
 | 
			
		||||
    await storage.setBoolean(_StorageKeys.muted, muted.value);
 | 
			
		||||
    await storage.setInt(_StorageKeys.crossAxisCount, crossAxisCount.value);
 | 
			
		||||
    await storage.setBoolean(
 | 
			
		||||
      _StorageKeys.drawerSwipeEnabled,
 | 
			
		||||
      drawerSwipeEnabled.value,
 | 
			
		||||
    );
 | 
			
		||||
    await storage.setInt(
 | 
			
		||||
      _StorageKeys.transitionType,
 | 
			
		||||
      transitionType.value.index,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/themecontroller.dart';
 | 
			
		||||
@@ -24,6 +25,7 @@ void main() async {
 | 
			
		||||
 | 
			
		||||
  Get.put(AuthController());
 | 
			
		||||
  Get.put(ApiService());
 | 
			
		||||
  Get.put(SettingsController());
 | 
			
		||||
  Get.put(MediaController());
 | 
			
		||||
 | 
			
		||||
  final ThemeController themeController = Get.put(ThemeController());
 | 
			
		||||
 
 | 
			
		||||
@@ -5,17 +5,20 @@ import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter_cache_manager/file.dart';
 | 
			
		||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:pullex/pullex.dart';
 | 
			
		||||
import 'package:share_plus/share_plus.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/services/api.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/favoritesection.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/fullscreen.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/tagsection.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
 | 
			
		||||
enum ShareAction { media, directLink, postLink }
 | 
			
		||||
@@ -31,10 +34,11 @@ class MediaDetailScreen extends StatefulWidget {
 | 
			
		||||
class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  PageController? _pageController;
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
  final SettingsController settingsController = Get.find<SettingsController>();
 | 
			
		||||
  final AuthController authController = Get.find<AuthController>();
 | 
			
		||||
  final RxInt _currentIndex = 0.obs;
 | 
			
		||||
  final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
			
		||||
  final Map<int, bool> _expandedTags = {};
 | 
			
		||||
  final Map<int, PullexRefreshController> _refreshControllers = {};
 | 
			
		||||
 | 
			
		||||
  bool _isLoading = true;
 | 
			
		||||
  bool _itemNotFound = false;
 | 
			
		||||
@@ -103,6 +107,31 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
      ..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _onRefresh(
 | 
			
		||||
    int itemId,
 | 
			
		||||
    PullexRefreshController controller,
 | 
			
		||||
  ) async {
 | 
			
		||||
    if (mediaController.loading.value) {
 | 
			
		||||
      controller.refreshCompleted();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final MediaItem item = await ApiService().fetchItemById(itemId);
 | 
			
		||||
      final int index = mediaController.items.indexWhere(
 | 
			
		||||
        (item) => item.id == itemId,
 | 
			
		||||
      );
 | 
			
		||||
      if (index != -1) {
 | 
			
		||||
        mediaController.items[index] = item;
 | 
			
		||||
        mediaController.items.refresh();
 | 
			
		||||
      }
 | 
			
		||||
      controller.refreshCompleted();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showMsg('Fehler beim Aktualisieren: $e');
 | 
			
		||||
      controller.refreshFailed();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onPageChanged(int idx) {
 | 
			
		||||
    if (idx != _currentIndex.value) {
 | 
			
		||||
      _currentIndex.value = idx;
 | 
			
		||||
@@ -148,7 +177,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
          item.mediaUrl,
 | 
			
		||||
        );
 | 
			
		||||
        final Uint8List bytes = await file.readAsBytes();
 | 
			
		||||
        final params = ShareParams(
 | 
			
		||||
        final ShareParams params = ShareParams(
 | 
			
		||||
          files: [XFile.fromData(bytes, mimeType: item.mime)],
 | 
			
		||||
        );
 | 
			
		||||
        await SharePlus.instance.share(params);
 | 
			
		||||
@@ -175,6 +204,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _pageController?.dispose();
 | 
			
		||||
    for (PullexRefreshController controller in _refreshControllers.values) {
 | 
			
		||||
      controller.dispose();
 | 
			
		||||
    }
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -217,70 +249,77 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Obx(
 | 
			
		||||
      () => PageView.builder(
 | 
			
		||||
        controller: _pageController!,
 | 
			
		||||
        itemCount: mediaController.items.length,
 | 
			
		||||
        onPageChanged: _onPageChanged,
 | 
			
		||||
        itemBuilder: (context, index) {
 | 
			
		||||
          final MediaItem item = mediaController.items[index];
 | 
			
		||||
          final bool isReady = _readyItemIds.contains(item.id);
 | 
			
		||||
          final bool areTagsExpanded = _expandedTags[item.id] ?? false;
 | 
			
		||||
          final List<Tag> allTags = item.tags ?? [];
 | 
			
		||||
          final bool hasMoreTags = allTags.length > 5;
 | 
			
		||||
          final List<Tag> tagsToShow = areTagsExpanded
 | 
			
		||||
              ? allTags
 | 
			
		||||
              : allTags.take(5).toList();
 | 
			
		||||
 | 
			
		||||
          return Obx(
 | 
			
		||||
            () => Scaffold(
 | 
			
		||||
              endDrawer: EndDrawer(),
 | 
			
		||||
    return Obx(() {
 | 
			
		||||
      if (mediaController.items.isEmpty ||
 | 
			
		||||
          _currentIndex.value >= mediaController.items.length) {
 | 
			
		||||
        return Scaffold(
 | 
			
		||||
          appBar: AppBar(title: const Text('Fehler')),
 | 
			
		||||
          body: const Center(child: Text('Keine Items zum Anzeigen.')),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      final MediaItem currentItem = mediaController.items[_currentIndex.value];
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        endDrawer: const EndDrawer(),
 | 
			
		||||
        endDrawerEnableOpenDragGesture:
 | 
			
		||||
                  mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
            settingsController.drawerSwipeEnabled.value,
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
                title: Text('f0ck #${item.id}'),
 | 
			
		||||
          title: Text('f0ck #${currentItem.id}'),
 | 
			
		||||
          actions: [
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.fullscreen),
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                Get.to(
 | 
			
		||||
                        FullScreenMediaView(item: item),
 | 
			
		||||
                  FullScreenMediaView(item: currentItem),
 | 
			
		||||
                  fullscreenDialog: true,
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: const Icon(Icons.download),
 | 
			
		||||
                    onPressed: () async {
 | 
			
		||||
                      await _downloadMedia(item);
 | 
			
		||||
                    },
 | 
			
		||||
              onPressed: () async => await _downloadMedia(currentItem),
 | 
			
		||||
            ),
 | 
			
		||||
            PopupMenuButton<ShareAction>(
 | 
			
		||||
                    onSelected: (value) => _handleShareAction(value, item),
 | 
			
		||||
              onSelected: (value) => _handleShareAction(value, currentItem),
 | 
			
		||||
              itemBuilder: (context) => _shareMenuItems,
 | 
			
		||||
              icon: const Icon(Icons.share),
 | 
			
		||||
            ),
 | 
			
		||||
            Builder(
 | 
			
		||||
              builder: (context) => IconButton(
 | 
			
		||||
                icon: const Icon(Icons.menu),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                      },
 | 
			
		||||
                onPressed: () => Scaffold.of(context).openEndDrawer(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
              body: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                children: [
 | 
			
		||||
                  AnimatedBuilder(
 | 
			
		||||
        body: PageView.builder(
 | 
			
		||||
          controller: _pageController!,
 | 
			
		||||
          itemCount: mediaController.items.length,
 | 
			
		||||
          onPageChanged: _onPageChanged,
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
            final MediaItem item = mediaController.items[index];
 | 
			
		||||
            final bool isReady = _readyItemIds.contains(item.id);
 | 
			
		||||
            final ScrollController scrollController = ScrollController();
 | 
			
		||||
            final PullexRefreshController refreshController =
 | 
			
		||||
                _refreshControllers.putIfAbsent(
 | 
			
		||||
                  item.id,
 | 
			
		||||
                  () => PullexRefreshController(),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            return PullexRefresh(
 | 
			
		||||
              onRefresh: () => _onRefresh(item.id, refreshController),
 | 
			
		||||
              header: const WaterDropHeader(),
 | 
			
		||||
              controller: refreshController,
 | 
			
		||||
              child: CustomScrollView(
 | 
			
		||||
                controller: scrollController,
 | 
			
		||||
                slivers: [
 | 
			
		||||
                  SliverToBoxAdapter(
 | 
			
		||||
                    child: AnimatedBuilder(
 | 
			
		||||
                      animation: _pageController!,
 | 
			
		||||
                      builder: (context, child) {
 | 
			
		||||
                        return buildAnimatedTransition(
 | 
			
		||||
                          context: context,
 | 
			
		||||
                          pageController: _pageController!,
 | 
			
		||||
                          index: index,
 | 
			
		||||
                        controller: mediaController,
 | 
			
		||||
                          child: child!,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
@@ -288,52 +327,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
                        () => _buildMedia(item, index == _currentIndex.value),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  Expanded(
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (isReady)
 | 
			
		||||
                    SliverToBoxAdapter(
 | 
			
		||||
                      child: GestureDetector(
 | 
			
		||||
                      onTap: () => mediaController.hideVideoControls(),
 | 
			
		||||
                        onTap: () => settingsController.hideVideoControls(),
 | 
			
		||||
                        behavior: HitTestBehavior.translucent,
 | 
			
		||||
                      child: Visibility(
 | 
			
		||||
                        visible: isReady,
 | 
			
		||||
                        child: SingleChildScrollView(
 | 
			
		||||
                        child: Padding(
 | 
			
		||||
                          padding: const EdgeInsets.all(16.0),
 | 
			
		||||
                          child: Column(
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                            children: [
 | 
			
		||||
                                Wrap(
 | 
			
		||||
                                  spacing: 6.0,
 | 
			
		||||
                                  runSpacing: 4.0,
 | 
			
		||||
                                  alignment: WrapAlignment.center,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    ...tagsToShow.map(
 | 
			
		||||
                                      (tag) => ActionTag(
 | 
			
		||||
                                        tag,
 | 
			
		||||
                                        (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
			
		||||
                                            ? (onTagTap) => {}
 | 
			
		||||
                                            : (onTagTap) {
 | 
			
		||||
                                                mediaController.setTag(
 | 
			
		||||
                                                  onTagTap,
 | 
			
		||||
                                                );
 | 
			
		||||
                                                Get.offAllNamed('/');
 | 
			
		||||
                                              },
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                                if (hasMoreTags)
 | 
			
		||||
                                  TextButton(
 | 
			
		||||
                                    onPressed: () {
 | 
			
		||||
                                      setState(
 | 
			
		||||
                                        () => _expandedTags[item.id] =
 | 
			
		||||
                                            !areTagsExpanded,
 | 
			
		||||
                                      );
 | 
			
		||||
                                    },
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      areTagsExpanded
 | 
			
		||||
                                          ? 'Weniger anzeigen'
 | 
			
		||||
                                          : 'Alle ${allTags.length} Tags anzeigen',
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                              TagSection(tags: item.tags ?? []),
 | 
			
		||||
                              Obx(
 | 
			
		||||
                                () => Visibility(
 | 
			
		||||
                                  visible: authController.isLoggedIn,
 | 
			
		||||
@@ -351,18 +356,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  const SliverToBoxAdapter(
 | 
			
		||||
                    child: SafeArea(child: SizedBox.shrink()),
 | 
			
		||||
                  ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SafeArea(child: SizedBox.shrink()),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              persistentFooterButtons: mediaController.tag.value != null
 | 
			
		||||
                  ? [TagFooter()]
 | 
			
		||||
                  : null,
 | 
			
		||||
            ),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        persistentFooterButtons: mediaController.tag.value != null
 | 
			
		||||
            ? [TagFooter()]
 | 
			
		||||
            : null,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/media_tile.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class MediaGrid extends StatefulWidget {
 | 
			
		||||
@@ -20,6 +21,7 @@ class MediaGrid extends StatefulWidget {
 | 
			
		||||
class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
  final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  final MediaController _mediaController = Get.put(MediaController());
 | 
			
		||||
  final SettingsController _settingsController = Get.put(SettingsController());
 | 
			
		||||
  final PullexRefreshController _refreshController = PullexRefreshController(
 | 
			
		||||
    initialRefresh: false,
 | 
			
		||||
  );
 | 
			
		||||
@@ -35,6 +37,7 @@ class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
    _body = _MediaGridBody(
 | 
			
		||||
      refreshController: _refreshController,
 | 
			
		||||
      mediaController: _mediaController,
 | 
			
		||||
      settingsController: _settingsController,
 | 
			
		||||
      scrollController: _scrollController,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -52,7 +55,7 @@ class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
      () => Scaffold(
 | 
			
		||||
        endDrawer: const EndDrawer(),
 | 
			
		||||
        endDrawerEnableOpenDragGesture:
 | 
			
		||||
            _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
            _settingsController.drawerSwipeEnabled.value,
 | 
			
		||||
        bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
        appBar: _appBar,
 | 
			
		||||
        body: _body,
 | 
			
		||||
@@ -121,11 +124,13 @@ class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
  const _MediaGridBody({
 | 
			
		||||
    required this.refreshController,
 | 
			
		||||
    required this.mediaController,
 | 
			
		||||
    required this.settingsController,
 | 
			
		||||
    required this.scrollController,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final PullexRefreshController refreshController;
 | 
			
		||||
  final MediaController mediaController;
 | 
			
		||||
  final SettingsController settingsController;
 | 
			
		||||
  final ScrollController scrollController;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -134,7 +139,7 @@ class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
      controller: refreshController,
 | 
			
		||||
      enablePullDown: true,
 | 
			
		||||
      enablePullUp: true,
 | 
			
		||||
      header: const MaterialHeader(),
 | 
			
		||||
      header: const WaterDropHeader(),
 | 
			
		||||
      onRefresh: () async {
 | 
			
		||||
        try {
 | 
			
		||||
          await mediaController.handleRefresh();
 | 
			
		||||
@@ -157,7 +162,7 @@ class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
          shrinkWrap: true,
 | 
			
		||||
          padding: const EdgeInsets.all(4),
 | 
			
		||||
          itemCount: mediaController.items.length,
 | 
			
		||||
          gridDelegate: mediaController.crossAxisCount.value == 0
 | 
			
		||||
          gridDelegate: settingsController.crossAxisCount.value == 0
 | 
			
		||||
              ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                  maxCrossAxisExtent: 150,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
@@ -165,7 +170,7 @@ class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
                )
 | 
			
		||||
              : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                  crossAxisCount: mediaController.crossAxisCount.value,
 | 
			
		||||
                  crossAxisCount: settingsController.crossAxisCount.value,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
                  mainAxisSpacing: 5,
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
 | 
			
		||||
class SettingsPage extends StatefulWidget {
 | 
			
		||||
@@ -15,8 +15,8 @@ class SettingsPage extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
  final MediaController controller = Get.find();
 | 
			
		||||
  final LocalizationController localizationController = Get.find();
 | 
			
		||||
  final SettingsController settingsController = Get.find<SettingsController>();
 | 
			
		||||
  final LocalizationController localizationController = Get.find<LocalizationController>();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -34,7 +34,7 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                title: Text('settings_numberofcolumns_title'.tr),
 | 
			
		||||
                trailing: Obx(
 | 
			
		||||
                  () => DropdownButton<int>(
 | 
			
		||||
                    value: controller.crossAxisCount.value,
 | 
			
		||||
                    value: settingsController.crossAxisCount.value,
 | 
			
		||||
                    dropdownColor: const Color.fromARGB(255, 43, 43, 43),
 | 
			
		||||
                    iconEnabledColor: Colors.white,
 | 
			
		||||
                    items: [0, 3, 4, 5].map((int value) {
 | 
			
		||||
@@ -51,7 +51,7 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    }).toList(),
 | 
			
		||||
                    onChanged: (int? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setCrossAxisCount(newValue);
 | 
			
		||||
                        await settingsController.setCrossAxisCount(newValue);
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -62,7 +62,7 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                title: Text('settings_pageanimation_title'.tr),
 | 
			
		||||
                trailing: Obx(
 | 
			
		||||
                  () => DropdownButton<PageTransition>(
 | 
			
		||||
                    value: controller.transitionType.value,
 | 
			
		||||
                    value: settingsController.transitionType.value,
 | 
			
		||||
                    dropdownColor: const Color.fromARGB(255, 43, 43, 43),
 | 
			
		||||
                    iconEnabledColor: Colors.white,
 | 
			
		||||
                    items: PageTransition.values.map((PageTransition type) {
 | 
			
		||||
@@ -91,7 +91,7 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    }).toList(),
 | 
			
		||||
                    onChanged: (PageTransition? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setTransitionType(newValue);
 | 
			
		||||
                        await settingsController.setTransitionType(newValue);
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -102,9 +102,9 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
              SwitchListTile(
 | 
			
		||||
                title: Text('settings_drawer_title'.tr),
 | 
			
		||||
                subtitle: Text('settings_drawer_subtitle'.tr),
 | 
			
		||||
                value: controller.drawerSwipeEnabled.value,
 | 
			
		||||
                value: settingsController.drawerSwipeEnabled.value,
 | 
			
		||||
                onChanged: (bool value) async {
 | 
			
		||||
                  await controller.setDrawerSwipeEnabled(value);
 | 
			
		||||
                  await settingsController.setDrawerSwipeEnabled(value);
 | 
			
		||||
                  setState(() {});
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/models/feed.dart';
 | 
			
		||||
 | 
			
		||||
class ApiService extends GetConnect {
 | 
			
		||||
  final EncryptedSharedPreferencesAsync storage =
 | 
			
		||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
			
		||||
  final AuthController _authController = Get.find<AuthController>();
 | 
			
		||||
 | 
			
		||||
  Future<Feed> fetchItems({
 | 
			
		||||
    int? older,
 | 
			
		||||
@@ -16,7 +15,7 @@ class ApiService extends GetConnect {
 | 
			
		||||
    int random = 0,
 | 
			
		||||
    String? tag,
 | 
			
		||||
  }) async {
 | 
			
		||||
    String? token = await storage.getString('token');
 | 
			
		||||
    String? token = _authController.token.value;
 | 
			
		||||
    final params = <String, String>{
 | 
			
		||||
      'type': type.toString(),
 | 
			
		||||
      'mode': mode.toString(),
 | 
			
		||||
@@ -42,18 +41,40 @@ class ApiService extends GetConnect {
 | 
			
		||||
      feed.items.sort((a, b) => b.id.compareTo(a.id));
 | 
			
		||||
      return feed;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (Get.isSnackbarOpen == false) {
 | 
			
		||||
      if (!Get.isSnackbarOpen) {
 | 
			
		||||
        Get.snackbar('Fehler', 'Fehler beim Laden der Items');
 | 
			
		||||
      }
 | 
			
		||||
      throw Exception('Fehler beim Laden der Items');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<MediaItem> fetchItemById(int itemId) async {
 | 
			
		||||
    String? token = _authController.token.value;
 | 
			
		||||
    final Map<String, String> headers = <String, String>{};
 | 
			
		||||
    if (token != null && token.isNotEmpty) {
 | 
			
		||||
      headers['Authorization'] = 'Bearer $token';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final Response<dynamic> response = await get(
 | 
			
		||||
      'https://api.f0ck.me/item/$itemId',
 | 
			
		||||
      headers: headers,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (response.status.code == 200 && response.body is Map<String, dynamic>) {
 | 
			
		||||
      return MediaItem.fromJson(response.body as Map<String, dynamic>);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!Get.isSnackbarOpen) {
 | 
			
		||||
        Get.snackbar('Fehler', 'Fehler beim Laden des Items');
 | 
			
		||||
      }
 | 
			
		||||
      throw Exception('Fehler beim Laden des Items');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Favorite>?> toggleFavorite(
 | 
			
		||||
    MediaItem item,
 | 
			
		||||
    bool isFavorite,
 | 
			
		||||
  ) async {
 | 
			
		||||
    String? token = await storage.getString('token');
 | 
			
		||||
    String? token = _authController.token.value;
 | 
			
		||||
    if (token == null || token.isEmpty) return null;
 | 
			
		||||
 | 
			
		||||
    final Map<String, String> headers = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
 | 
			
		||||
enum PageTransition { opacity, scale, slide, rotate, flip }
 | 
			
		||||
 | 
			
		||||
@@ -9,17 +11,17 @@ Widget buildAnimatedTransition({
 | 
			
		||||
  required Widget child,
 | 
			
		||||
  required PageController pageController,
 | 
			
		||||
  required int index,
 | 
			
		||||
  required MediaController controller,
 | 
			
		||||
}) {
 | 
			
		||||
  final SettingsController settingsController = Get.find<SettingsController>();
 | 
			
		||||
  final double value = pageController.position.haveDimensions
 | 
			
		||||
      ? pageController.page! - index
 | 
			
		||||
      : 0;
 | 
			
		||||
 | 
			
		||||
  switch (controller.transitionType.value) {
 | 
			
		||||
  switch (settingsController.transitionType.value) {
 | 
			
		||||
    case PageTransition.opacity:
 | 
			
		||||
      return Opacity(
 | 
			
		||||
        opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)),
 | 
			
		||||
        child: Transform(transform: Matrix4.identity(), child: child),
 | 
			
		||||
        child: child,
 | 
			
		||||
      );
 | 
			
		||||
    case PageTransition.scale:
 | 
			
		||||
      return Transform.scale(
 | 
			
		||||
@@ -29,10 +31,7 @@ Widget buildAnimatedTransition({
 | 
			
		||||
        child: child,
 | 
			
		||||
      );
 | 
			
		||||
    case PageTransition.slide:
 | 
			
		||||
      return Transform.translate(
 | 
			
		||||
        offset: Offset(300 * value.abs(), 0),
 | 
			
		||||
        child: child,
 | 
			
		||||
      );
 | 
			
		||||
      return child;
 | 
			
		||||
    case PageTransition.rotate:
 | 
			
		||||
      return Opacity(
 | 
			
		||||
        opacity: (1 - value.abs()).clamp(0.0, 1.0),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ class MediaTile extends StatelessWidget {
 | 
			
		||||
          fit: StackFit.expand,
 | 
			
		||||
          children: [
 | 
			
		||||
            CachedNetworkImage(
 | 
			
		||||
              imageUrl: 'https://f0ck.me/t/${item.id}.webp',
 | 
			
		||||
              imageUrl: item.thumbnailUrl,
 | 
			
		||||
              fit: BoxFit.cover,
 | 
			
		||||
              placeholder: (context, url) => Container(color: Colors.grey[900]),
 | 
			
		||||
              errorWidget: (context, url, error) =>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								lib/widgets/tagsection.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								lib/widgets/tagsection.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/actiontag.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class TagSection extends StatefulWidget {
 | 
			
		||||
  final List<Tag> tags;
 | 
			
		||||
  const TagSection({super.key, required this.tags});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<TagSection> createState() => _TagSectionState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _TagSectionState extends State<TagSection> {
 | 
			
		||||
  bool _areTagsExpanded = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
    final bool hasMoreTags = widget.tags.length > 5;
 | 
			
		||||
    final List<Tag> tagsToShow = _areTagsExpanded
 | 
			
		||||
        ? widget.tags
 | 
			
		||||
        : widget.tags.take(5).toList();
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        Wrap(
 | 
			
		||||
          spacing: 6.0,
 | 
			
		||||
          runSpacing: 4.0,
 | 
			
		||||
          alignment: WrapAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            ...tagsToShow.map(
 | 
			
		||||
              (tag) => ActionTag(
 | 
			
		||||
                tag,
 | 
			
		||||
                (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
			
		||||
                    ? (onTagTap) => {}
 | 
			
		||||
                    : (onTagTap) {
 | 
			
		||||
                        mediaController.setTag(onTagTap);
 | 
			
		||||
                        Get.offAllNamed('/');
 | 
			
		||||
                      },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        if (hasMoreTags)
 | 
			
		||||
          TextButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              setState(() => _areTagsExpanded = !_areTagsExpanded);
 | 
			
		||||
            },
 | 
			
		||||
            child: Text(
 | 
			
		||||
              _areTagsExpanded
 | 
			
		||||
                  ? 'Weniger anzeigen'
 | 
			
		||||
                  : 'Alle ${widget.tags.length} Tags anzeigen',
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -94,33 +94,39 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        _ControlButton(
 | 
			
		||||
        IconButton(
 | 
			
		||||
          icon: Icon(
 | 
			
		||||
            widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
			
		||||
          () {
 | 
			
		||||
            size: 64,
 | 
			
		||||
          ),
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            widget.onOverlayTap();
 | 
			
		||||
            widget.controller.value.isPlaying
 | 
			
		||||
                ? widget.controller.pause()
 | 
			
		||||
                : widget.controller.play();
 | 
			
		||||
          },
 | 
			
		||||
          size: 64,
 | 
			
		||||
        ),
 | 
			
		||||
        Positioned(
 | 
			
		||||
          right: 12,
 | 
			
		||||
          bottom: 12,
 | 
			
		||||
          child: _ControlButton(
 | 
			
		||||
          child: IconButton(
 | 
			
		||||
            icon: Icon(
 | 
			
		||||
              widget.muted ? Icons.volume_off : Icons.volume_up,
 | 
			
		||||
            () {
 | 
			
		||||
              size: 16,
 | 
			
		||||
            ),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              widget.onOverlayTap();
 | 
			
		||||
              widget.onMuteToggle();
 | 
			
		||||
            },
 | 
			
		||||
            size: 16,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        Align(
 | 
			
		||||
          alignment: Alignment.bottomCenter,
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(bottom: 0),
 | 
			
		||||
            child: Stack(
 | 
			
		||||
            child: LayoutBuilder(
 | 
			
		||||
              builder: (context, constraints) {
 | 
			
		||||
                return Stack(
 | 
			
		||||
                  clipBehavior: Clip.none,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Positioned(
 | 
			
		||||
@@ -128,7 +134,10 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
                      bottom: 12,
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
 | 
			
		||||
                    style: const TextStyle(color: Colors.white, fontSize: 12),
 | 
			
		||||
                        style: const TextStyle(
 | 
			
		||||
                          color: Colors.white,
 | 
			
		||||
                          fontSize: 12,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Listener(
 | 
			
		||||
@@ -139,10 +148,14 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
                        widget.controller,
 | 
			
		||||
                        allowScrubbing: true,
 | 
			
		||||
                        padding: const EdgeInsets.only(top: 25.0),
 | 
			
		||||
                    colors: const VideoProgressColors(
 | 
			
		||||
                      playedColor: Colors.red,
 | 
			
		||||
                      backgroundColor: Colors.grey,
 | 
			
		||||
                      bufferedColor: Colors.white54,
 | 
			
		||||
                        colors: VideoProgressColors(
 | 
			
		||||
                          playedColor: Theme.of(context).colorScheme.primary,
 | 
			
		||||
                          backgroundColor: Theme.of(
 | 
			
		||||
                            context,
 | 
			
		||||
                          ).colorScheme.surface.withValues(alpha: 0.5),
 | 
			
		||||
                          bufferedColor: Theme.of(
 | 
			
		||||
                            context,
 | 
			
		||||
                          ).colorScheme.secondary.withValues(alpha: 0.5),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -155,7 +168,7 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
                                        .value
 | 
			
		||||
                                        .duration
 | 
			
		||||
                                        .inMilliseconds) *
 | 
			
		||||
                            MediaQuery.of(context).size.width -
 | 
			
		||||
                                constraints.maxWidth -
 | 
			
		||||
                            6,
 | 
			
		||||
                        bottom: -4,
 | 
			
		||||
                        child: Container(
 | 
			
		||||
@@ -163,12 +176,17 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
                          height: 12,
 | 
			
		||||
                          decoration: BoxDecoration(
 | 
			
		||||
                            shape: BoxShape.circle,
 | 
			
		||||
                        color: Colors.red,
 | 
			
		||||
                        border: Border.all(color: Colors.red, width: 2),
 | 
			
		||||
                            color: Theme.of(context).colorScheme.primary,
 | 
			
		||||
                            border: Border.all(
 | 
			
		||||
                              color: Theme.of(context).colorScheme.primary,
 | 
			
		||||
                              width: 2,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
@@ -182,25 +200,3 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
			
		||||
    return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ControlButton extends StatelessWidget {
 | 
			
		||||
  final IconData icon;
 | 
			
		||||
  final VoidCallback onPressed;
 | 
			
		||||
  final double size;
 | 
			
		||||
 | 
			
		||||
  const _ControlButton(this.icon, this.onPressed, {this.size = 24});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      decoration: BoxDecoration(
 | 
			
		||||
        shape: BoxShape.circle,
 | 
			
		||||
        color: Colors.black.withValues(alpha: 0.4),
 | 
			
		||||
      ),
 | 
			
		||||
      child: IconButton(
 | 
			
		||||
        icon: Icon(icon, size: size),
 | 
			
		||||
        onPressed: onPressed,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_controls_overlay.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class VideoWidget extends StatefulWidget {
 | 
			
		||||
@@ -29,8 +30,9 @@ class VideoWidget extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  final MediaController controller = Get.find<MediaController>();
 | 
			
		||||
  late CachedVideoPlayerPlusController _controller;
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
  final SettingsController settingsController = Get.find<SettingsController>();
 | 
			
		||||
  late CachedVideoPlayerPlusController videoController;
 | 
			
		||||
  late Worker _muteWorker;
 | 
			
		||||
  late Worker _timerResetWorker;
 | 
			
		||||
  late Worker _hideControlsWorker;
 | 
			
		||||
@@ -41,12 +43,14 @@ 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);
 | 
			
		||||
    _muteWorker = ever(settingsController.muted, (bool muted) {
 | 
			
		||||
      if (videoController.value.isInitialized) {
 | 
			
		||||
        videoController.setVolume(muted ? 0.0 : 1.0);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    _timerResetWorker = ever(controller.videoControlsTimerNotifier, (_) {
 | 
			
		||||
    _timerResetWorker = ever(settingsController.videoControlsTimerNotifier, (
 | 
			
		||||
      _,
 | 
			
		||||
    ) {
 | 
			
		||||
      if (widget.isActive && mounted) {
 | 
			
		||||
        if (!_showControls) {
 | 
			
		||||
          setState(() => _showControls = true);
 | 
			
		||||
@@ -54,7 +58,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
        _startHideControlsTimer();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    _hideControlsWorker = ever(controller.hideControlsNotifier, (_) {
 | 
			
		||||
    _hideControlsWorker = ever(settingsController.hideControlsNotifier, (_) {
 | 
			
		||||
      if (mounted && _showControls) {
 | 
			
		||||
        setState(() => _showControls = false);
 | 
			
		||||
        _hideControlsTimer?.cancel();
 | 
			
		||||
@@ -63,20 +67,18 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _initController() async {
 | 
			
		||||
    _controller = CachedVideoPlayerPlusController.networkUrl(
 | 
			
		||||
    videoController = CachedVideoPlayerPlusController.networkUrl(
 | 
			
		||||
      Uri.parse(widget.details.mediaUrl),
 | 
			
		||||
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
 | 
			
		||||
    );
 | 
			
		||||
    await _controller.initialize();
 | 
			
		||||
    await videoController.initialize();
 | 
			
		||||
    widget.onInitialized?.call();
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    setState(() {});
 | 
			
		||||
    _controller.addListener(() => setState(() {}));
 | 
			
		||||
    _controller.setLooping(true);
 | 
			
		||||
    _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
 | 
			
		||||
    videoController.setLooping(true);
 | 
			
		||||
    videoController.setVolume(settingsController.muted.value ? 0.0 : 1.0);
 | 
			
		||||
 | 
			
		||||
    if (widget.isActive) {
 | 
			
		||||
      _controller.play();
 | 
			
		||||
      videoController.play();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -85,9 +87,9 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
    super.didUpdateWidget(oldWidget);
 | 
			
		||||
    if (widget.isActive != oldWidget.isActive) {
 | 
			
		||||
      if (widget.isActive) {
 | 
			
		||||
        _controller.play();
 | 
			
		||||
        videoController.play();
 | 
			
		||||
      } else {
 | 
			
		||||
        _controller.pause();
 | 
			
		||||
        videoController.pause();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -97,7 +99,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
    _muteWorker.dispose();
 | 
			
		||||
    _timerResetWorker.dispose();
 | 
			
		||||
    _hideControlsWorker.dispose();
 | 
			
		||||
    _controller.dispose();
 | 
			
		||||
    videoController.dispose();
 | 
			
		||||
    _hideControlsTimer?.cancel();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
@@ -129,7 +131,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final bool muted = controller.muted.value;
 | 
			
		||||
    final bool muted = settingsController.muted.value;
 | 
			
		||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
			
		||||
 | 
			
		||||
    Widget mediaContent;
 | 
			
		||||
@@ -144,35 +146,42 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      mediaContent = _controller.value.isInitialized
 | 
			
		||||
          ? CachedVideoPlayerPlus(_controller)
 | 
			
		||||
      mediaContent = videoController.value.isInitialized
 | 
			
		||||
          ? CachedVideoPlayerPlus(videoController)
 | 
			
		||||
          : const Center(child: CircularProgressIndicator());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AspectRatio(
 | 
			
		||||
      aspectRatio: _controller.value.isInitialized
 | 
			
		||||
          ? _controller.value.aspectRatio
 | 
			
		||||
      aspectRatio: videoController.value.isInitialized
 | 
			
		||||
          ? videoController.value.aspectRatio
 | 
			
		||||
          : (isAudio ? 16 / 9 : 9 / 16),
 | 
			
		||||
      child: Stack(
 | 
			
		||||
        alignment: Alignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          GestureDetector(onTap: _onTap, child: mediaContent),
 | 
			
		||||
          if (_controller.value.isInitialized && _showControls)
 | 
			
		||||
            Positioned.fill(
 | 
			
		||||
          AnimatedBuilder(
 | 
			
		||||
            animation: videoController,
 | 
			
		||||
            builder: (context, child) {
 | 
			
		||||
              if (videoController.value.isInitialized && _showControls) {
 | 
			
		||||
                return Positioned.fill(
 | 
			
		||||
                  child: GestureDetector(
 | 
			
		||||
                    onTap: _onTap,
 | 
			
		||||
                    child: Container(
 | 
			
		||||
                      color: Colors.black.withValues(alpha: 0.5),
 | 
			
		||||
                      child: VideoControlsOverlay(
 | 
			
		||||
                    controller: _controller,
 | 
			
		||||
                        controller: videoController,
 | 
			
		||||
                        onOverlayTap: () => _onTap(ctrlButton: true),
 | 
			
		||||
                        muted: muted,
 | 
			
		||||
                        onMuteToggle: () {
 | 
			
		||||
                      controller.toggleMuted();
 | 
			
		||||
                          settingsController.toggleMuted();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
              return const SizedBox.shrink();
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 1.4.3+64
 | 
			
		||||
version: 1.4.4+65
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.9.0-100.2.beta
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user