...
This commit is contained in:
		@@ -41,7 +41,7 @@ jobs:
 | 
				
			|||||||
          TAR_OPTIONS: --no-same-owner
 | 
					          TAR_OPTIONS: --no-same-owner
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      - name: build apk
 | 
					      - name: build apk
 | 
				
			||||||
        run: flutter build apk --release --split-per-abi -v
 | 
					        run: flutter build apk --release --split-per-abi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: release-build
 | 
					      - name: release-build
 | 
				
			||||||
        uses: akkuman/gitea-release-action@v1
 | 
					        uses: akkuman/gitea-release-action@v1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,9 +19,6 @@ class SettingsController extends GetxController {
 | 
				
			|||||||
  RxBool drawerSwipeEnabled = true.obs;
 | 
					  RxBool drawerSwipeEnabled = true.obs;
 | 
				
			||||||
  RxInt crossAxisCount = 0.obs;
 | 
					  RxInt crossAxisCount = 0.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  RxInt videoControlsTimerNotifier = 0.obs;
 | 
					 | 
				
			||||||
  RxInt hideControlsNotifier = 0.obs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void onInit() {
 | 
					  void onInit() {
 | 
				
			||||||
    super.onInit();
 | 
					    super.onInit();
 | 
				
			||||||
@@ -53,9 +50,6 @@ class SettingsController extends GetxController {
 | 
				
			|||||||
    await saveSettings();
 | 
					    await saveSettings();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void resetVideoControlsTimer() => videoControlsTimerNotifier.value++;
 | 
					 | 
				
			||||||
  void hideVideoControls() => hideControlsNotifier.value++;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> loadSettings() async {
 | 
					  Future<void> loadSettings() async {
 | 
				
			||||||
    muted.value = await storage.getBoolean(_StorageKeys.muted) ?? false;
 | 
					    muted.value = await storage.getBoolean(_StorageKeys.muted) ?? false;
 | 
				
			||||||
    crossAxisCount.value =
 | 
					    crossAxisCount.value =
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,14 +8,21 @@ import 'package:f0ckapp/widgets/video_widget.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class FullScreenMediaView extends StatefulWidget {
 | 
					class FullScreenMediaView extends StatefulWidget {
 | 
				
			||||||
  final MediaItem item;
 | 
					  final MediaItem item;
 | 
				
			||||||
 | 
					  final Duration? initialPosition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const FullScreenMediaView({super.key, required this.item});
 | 
					  const FullScreenMediaView({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.item,
 | 
				
			||||||
 | 
					    this.initialPosition,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State createState() => _FullScreenMediaViewState();
 | 
					  State createState() => _FullScreenMediaViewState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
					class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
				
			||||||
 | 
					  final GlobalKey<VideoWidgetState> _videoKey = GlobalKey<VideoWidgetState>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
@@ -30,9 +37,21 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
				
			|||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _popWithPosition() {
 | 
				
			||||||
 | 
					    Duration? currentPosition;
 | 
				
			||||||
 | 
					    if (widget.item.mime.startsWith('video') && _videoKey.currentState != null) {
 | 
				
			||||||
 | 
					      currentPosition = _videoKey.currentState!.videoController.value.position;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Navigator.of(context).pop(currentPosition);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return PopScope(
 | 
				
			||||||
 | 
					      onPopInvokedWithResult: (bool didPop, Object? result) async {
 | 
				
			||||||
 | 
					        return _popWithPosition();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      child: Scaffold(
 | 
				
			||||||
        backgroundColor: Colors.black,
 | 
					        backgroundColor: Colors.black,
 | 
				
			||||||
        body: Stack(
 | 
					        body: Stack(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
@@ -52,9 +71,11 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                  : Center(
 | 
					                  : Center(
 | 
				
			||||||
                      child: VideoWidget(
 | 
					                      child: VideoWidget(
 | 
				
			||||||
 | 
					                        key: _videoKey,
 | 
				
			||||||
                        details: widget.item,
 | 
					                        details: widget.item,
 | 
				
			||||||
                        isActive: true,
 | 
					                        isActive: true,
 | 
				
			||||||
                        fullScreen: true,
 | 
					                        fullScreen: true,
 | 
				
			||||||
 | 
					                        initialPosition: widget.initialPosition,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -63,12 +84,13 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
				
			|||||||
                alignment: Alignment.topLeft,
 | 
					                alignment: Alignment.topLeft,
 | 
				
			||||||
                child: IconButton(
 | 
					                child: IconButton(
 | 
				
			||||||
                  icon: const Icon(Icons.arrow_back, color: Colors.white),
 | 
					                  icon: const Icon(Icons.arrow_back, color: Colors.white),
 | 
				
			||||||
                onPressed: () => Navigator.of(context).pop(),
 | 
					                  onPressed: _popWithPosition,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,10 +39,12 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
  final RxInt _currentIndex = 0.obs;
 | 
					  final RxInt _currentIndex = 0.obs;
 | 
				
			||||||
  final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
					  final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
				
			||||||
  final Map<int, PullexRefreshController> _refreshControllers = {};
 | 
					  final Map<int, PullexRefreshController> _refreshControllers = {};
 | 
				
			||||||
 | 
					  final Map<int, GlobalKey<VideoWidgetState>> _videoWidgetKeys = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _isLoading = true;
 | 
					  bool _isLoading = true;
 | 
				
			||||||
  bool _itemNotFound = false;
 | 
					  bool _itemNotFound = false;
 | 
				
			||||||
  final Set<int> _readyItemIds = {};
 | 
					  final Set<int> _readyItemIds = {};
 | 
				
			||||||
 | 
					  final Map<int, bool> _showFavoriteAnimation = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final List<PopupMenuEntry<ShareAction>> _shareMenuItems = const [
 | 
					  final List<PopupMenuEntry<ShareAction>> _shareMenuItems = const [
 | 
				
			||||||
    PopupMenuItem(
 | 
					    PopupMenuItem(
 | 
				
			||||||
@@ -140,6 +142,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
        setState(() => _readyItemIds.add(item.id));
 | 
					        setState(() => _readyItemIds.add(item.id));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (idx + 1 < mediaController.items.length) {
 | 
				
			||||||
 | 
					      DefaultCacheManager().downloadFile(
 | 
				
			||||||
 | 
					        mediaController.items[idx + 1].mediaUrl,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (idx - 1 >= 0) {
 | 
				
			||||||
 | 
					      DefaultCacheManager().downloadFile(
 | 
				
			||||||
 | 
					        mediaController.items[idx - 1].mediaUrl,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (idx >= mediaController.items.length - 2 &&
 | 
					    if (idx >= mediaController.items.length - 2 &&
 | 
				
			||||||
        !mediaController.loading.value &&
 | 
					        !mediaController.loading.value &&
 | 
				
			||||||
        !mediaController.atEnd.value) {
 | 
					        !mediaController.atEnd.value) {
 | 
				
			||||||
@@ -201,6 +215,37 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _handleFullScreen(MediaItem currentItem) async {
 | 
				
			||||||
 | 
					    if (currentItem.mime.startsWith('image')) {
 | 
				
			||||||
 | 
					      Get.to(
 | 
				
			||||||
 | 
					        () => FullScreenMediaView(item: currentItem),
 | 
				
			||||||
 | 
					        fullscreenDialog: true,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final GlobalKey<VideoWidgetState>? key = _videoWidgetKeys[currentItem.id];
 | 
				
			||||||
 | 
					    final VideoWidgetState? videoState = key?.currentState;
 | 
				
			||||||
 | 
					    if (videoState == null || !videoState.videoController.value.isInitialized) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Duration position = videoState.videoController.value.position;
 | 
				
			||||||
 | 
					    await videoState.videoController.pause();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Duration? newPosition = await Get.to<Duration?>(
 | 
				
			||||||
 | 
					      () => FullScreenMediaView(item: currentItem, initialPosition: position),
 | 
				
			||||||
 | 
					      fullscreenDialog: true,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (mounted && videoState.mounted) {
 | 
				
			||||||
 | 
					      if (newPosition != null) {
 | 
				
			||||||
 | 
					        await videoState.videoController.seekTo(newPosition);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await videoState.videoController.play();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _pageController?.dispose();
 | 
					    _pageController?.dispose();
 | 
				
			||||||
@@ -210,16 +255,49 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _handleFavoriteToggle(MediaItem item, bool isFavorite) async {
 | 
				
			||||||
 | 
					    if (!authController.isLoggedIn) return;
 | 
				
			||||||
 | 
					    HapticFeedback.lightImpact();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final List<Favorite>? newFavorites = await mediaController.toggleFavorite(
 | 
				
			||||||
 | 
					      item,
 | 
				
			||||||
 | 
					      isFavorite,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final int index = mediaController.items.indexWhere((i) => i.id == item.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newFavorites != null && index != -1) {
 | 
				
			||||||
 | 
					      mediaController.items[index] = item.copyWith(favorites: newFavorites);
 | 
				
			||||||
 | 
					      mediaController.items.refresh();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					    setState(() => _showFavoriteAnimation[item.id] = true);
 | 
				
			||||||
 | 
					    Future.delayed(const Duration(milliseconds: 700), () {
 | 
				
			||||||
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        setState(() => _showFavoriteAnimation[item.id] = false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget _buildMedia(MediaItem item, bool isActive) {
 | 
					  Widget _buildMedia(MediaItem item, bool isActive) {
 | 
				
			||||||
 | 
					    Widget mediaWidget;
 | 
				
			||||||
    if (item.mime.startsWith('image/')) {
 | 
					    if (item.mime.startsWith('image/')) {
 | 
				
			||||||
      return CachedNetworkImage(
 | 
					      mediaWidget = CachedNetworkImage(
 | 
				
			||||||
        imageUrl: item.mediaUrl,
 | 
					        imageUrl: item.mediaUrl,
 | 
				
			||||||
        fit: BoxFit.contain,
 | 
					        fit: BoxFit.contain,
 | 
				
			||||||
 | 
					        placeholder: (context, url) =>
 | 
				
			||||||
 | 
					            const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
        errorWidget: (c, e, s) => const Icon(Icons.broken_image, size: 100),
 | 
					        errorWidget: (c, e, s) => const Icon(Icons.broken_image, size: 100),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else if (item.mime.startsWith('video/') ||
 | 
					    } else if (item.mime.startsWith('video/') ||
 | 
				
			||||||
        item.mime.startsWith('audio/')) {
 | 
					        item.mime.startsWith('audio/')) {
 | 
				
			||||||
      return VideoWidget(
 | 
					      final key = _videoWidgetKeys.putIfAbsent(
 | 
				
			||||||
 | 
					        item.id,
 | 
				
			||||||
 | 
					        () => GlobalKey<VideoWidgetState>(),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      mediaWidget = VideoWidget(
 | 
				
			||||||
 | 
					        key: key,
 | 
				
			||||||
        details: item,
 | 
					        details: item,
 | 
				
			||||||
        isActive: isActive,
 | 
					        isActive: isActive,
 | 
				
			||||||
        onInitialized: () {
 | 
					        onInitialized: () {
 | 
				
			||||||
@@ -229,8 +307,39 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return const Icon(Icons.help_outline, size: 100);
 | 
					      mediaWidget = const Icon(Icons.help_outline, size: 100);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final bool isFavorite =
 | 
				
			||||||
 | 
					        item.favorites?.any((f) => f.userId == authController.user.value?.id) ??
 | 
				
			||||||
 | 
					        false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Hero(
 | 
				
			||||||
 | 
					      tag: 'media_${item.id}',
 | 
				
			||||||
 | 
					      child: GestureDetector(
 | 
				
			||||||
 | 
					        onDoubleTap: () => _handleFavoriteToggle(item, isFavorite),
 | 
				
			||||||
 | 
					        child: Stack(
 | 
				
			||||||
 | 
					          alignment: Alignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            mediaWidget,
 | 
				
			||||||
 | 
					            AnimatedOpacity(
 | 
				
			||||||
 | 
					              opacity: _showFavoriteAnimation[item.id] ?? false ? 1.0 : 0.0,
 | 
				
			||||||
 | 
					              duration: const Duration(milliseconds: 200),
 | 
				
			||||||
 | 
					              child: AnimatedScale(
 | 
				
			||||||
 | 
					                scale: _showFavoriteAnimation[item.id] ?? false ? 1.0 : 0.5,
 | 
				
			||||||
 | 
					                duration: const Duration(milliseconds: 400),
 | 
				
			||||||
 | 
					                curve: Curves.easeOutBack,
 | 
				
			||||||
 | 
					                child: Icon(
 | 
				
			||||||
 | 
					                  isFavorite ? Icons.favorite : Icons.favorite_outline,
 | 
				
			||||||
 | 
					                  color: Colors.red,
 | 
				
			||||||
 | 
					                  size: 100,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -267,12 +376,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
          actions: [
 | 
					          actions: [
 | 
				
			||||||
            IconButton(
 | 
					            IconButton(
 | 
				
			||||||
              icon: const Icon(Icons.fullscreen),
 | 
					              icon: const Icon(Icons.fullscreen),
 | 
				
			||||||
              onPressed: () {
 | 
					              onPressed: () => _handleFullScreen(currentItem),
 | 
				
			||||||
                Get.to(
 | 
					 | 
				
			||||||
                  FullScreenMediaView(item: currentItem),
 | 
					 | 
				
			||||||
                  fullscreenDialog: true,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            IconButton(
 | 
					            IconButton(
 | 
				
			||||||
              icon: const Icon(Icons.download),
 | 
					              icon: const Icon(Icons.download),
 | 
				
			||||||
@@ -291,28 +395,26 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        body: PageView.builder(
 | 
					        body: Stack(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            PageView.builder(
 | 
				
			||||||
              controller: _pageController!,
 | 
					              controller: _pageController!,
 | 
				
			||||||
              itemCount: mediaController.items.length,
 | 
					              itemCount: mediaController.items.length,
 | 
				
			||||||
              onPageChanged: _onPageChanged,
 | 
					              onPageChanged: _onPageChanged,
 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                final MediaItem item = mediaController.items[index];
 | 
					                final MediaItem item = mediaController.items[index];
 | 
				
			||||||
                final bool isReady = _readyItemIds.contains(item.id);
 | 
					                final bool isReady = _readyItemIds.contains(item.id);
 | 
				
			||||||
            final ScrollController scrollController = ScrollController();
 | 
					 | 
				
			||||||
                final PullexRefreshController refreshController =
 | 
					                final PullexRefreshController refreshController =
 | 
				
			||||||
                    _refreshControllers.putIfAbsent(
 | 
					                    _refreshControllers.putIfAbsent(
 | 
				
			||||||
                      item.id,
 | 
					                      item.id,
 | 
				
			||||||
                      () => PullexRefreshController(),
 | 
					                      () => PullexRefreshController(),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Stack(
 | 
					                return PullexRefresh(
 | 
				
			||||||
              children: [
 | 
					 | 
				
			||||||
                PullexRefresh(
 | 
					 | 
				
			||||||
                  onRefresh: () => _onRefresh(item.id, refreshController),
 | 
					                  onRefresh: () => _onRefresh(item.id, refreshController),
 | 
				
			||||||
                  header: const WaterDropHeader(),
 | 
					                  header: const WaterDropHeader(),
 | 
				
			||||||
                  controller: refreshController,
 | 
					                  controller: refreshController,
 | 
				
			||||||
                  child: CustomScrollView(
 | 
					                  child: CustomScrollView(
 | 
				
			||||||
                    controller: scrollController,
 | 
					 | 
				
			||||||
                    slivers: [
 | 
					                    slivers: [
 | 
				
			||||||
                      SliverToBoxAdapter(
 | 
					                      SliverToBoxAdapter(
 | 
				
			||||||
                        child: AnimatedBuilder(
 | 
					                        child: AnimatedBuilder(
 | 
				
			||||||
@@ -335,9 +437,6 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
                        SliverFillRemaining(
 | 
					                        SliverFillRemaining(
 | 
				
			||||||
                          hasScrollBody: false,
 | 
					                          hasScrollBody: false,
 | 
				
			||||||
                          fillOverscroll: true,
 | 
					                          fillOverscroll: true,
 | 
				
			||||||
                          child: GestureDetector(
 | 
					 | 
				
			||||||
                            onTap: () => settingsController.hideVideoControls(),
 | 
					 | 
				
			||||||
                            behavior: HitTestBehavior.translucent,
 | 
					 | 
				
			||||||
                          child: Padding(
 | 
					                          child: Padding(
 | 
				
			||||||
                            padding: const EdgeInsets.all(16.0),
 | 
					                            padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                            child: Column(
 | 
					                            child: Column(
 | 
				
			||||||
@@ -346,12 +445,13 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      const SliverToBoxAdapter(
 | 
					                      const SliverToBoxAdapter(
 | 
				
			||||||
                        child: SafeArea(child: SizedBox.shrink()),
 | 
					                        child: SafeArea(child: SizedBox.shrink()),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Obx(() {
 | 
					            Obx(() {
 | 
				
			||||||
              if (!authController.isLoggedIn) {
 | 
					              if (!authController.isLoggedIn) {
 | 
				
			||||||
@@ -360,7 +460,8 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
              final MediaItem currentItem =
 | 
					              final MediaItem currentItem =
 | 
				
			||||||
                  mediaController.items[_currentIndex.value];
 | 
					                  mediaController.items[_currentIndex.value];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  final bool hasSoftButtons = MediaQuery.of(context).padding.bottom > 24.0;
 | 
					              final bool hasSoftButtons =
 | 
				
			||||||
 | 
					                  MediaQuery.of(context).padding.bottom > 24.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return DraggableScrollableSheet(
 | 
					              return DraggableScrollableSheet(
 | 
				
			||||||
                initialChildSize: hasSoftButtons ? 0.11 : 0.2,
 | 
					                initialChildSize: hasSoftButtons ? 0.11 : 0.2,
 | 
				
			||||||
@@ -397,8 +498,6 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
              );
 | 
					              );
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        persistentFooterButtons: mediaController.tag.value != null
 | 
					        persistentFooterButtons: mediaController.tag.value != null
 | 
				
			||||||
            ? [TagFooter()]
 | 
					            ? [TagFooter()]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:pullex/pullex.dart';
 | 
					import 'package:pullex/pullex.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
					import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
					import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
@@ -177,11 +178,17 @@ class _MediaGridBody extends StatelessWidget {
 | 
				
			|||||||
                  childAspectRatio: 1,
 | 
					                  childAspectRatio: 1,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
          itemBuilder: (context, index) {
 | 
					          itemBuilder: (context, index) {
 | 
				
			||||||
            final item = mediaController.items[index];
 | 
					            final MediaItem item = mediaController.items[index];
 | 
				
			||||||
            return GestureDetector(
 | 
					            return Hero(
 | 
				
			||||||
 | 
					              tag: 'media_${item.id}',
 | 
				
			||||||
 | 
					              child: Material(
 | 
				
			||||||
 | 
					                type: MaterialType.transparency,
 | 
				
			||||||
 | 
					                child: GestureDetector(
 | 
				
			||||||
                  key: ValueKey(item.id),
 | 
					                  key: ValueKey(item.id),
 | 
				
			||||||
                  onTap: () => Get.toNamed('/${item.id}'),
 | 
					                  onTap: () => Get.toNamed('/${item.id}'),
 | 
				
			||||||
                  child: MediaTile(item: item),
 | 
					                  child: MediaTile(item: item),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,197 +1,111 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/controller/settingscontroller.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoControlsOverlay extends StatefulWidget {
 | 
					class VideoControlsOverlay extends StatefulWidget {
 | 
				
			||||||
  final CachedVideoPlayerPlusController controller;
 | 
					  final CachedVideoPlayerPlusController controller;
 | 
				
			||||||
  final VoidCallback onOverlayTap;
 | 
					 | 
				
			||||||
  final bool muted;
 | 
					 | 
				
			||||||
  final VoidCallback onMuteToggle;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const VideoControlsOverlay({
 | 
					  const VideoControlsOverlay({super.key, required this.controller});
 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.controller,
 | 
					 | 
				
			||||||
    required this.onOverlayTap,
 | 
					 | 
				
			||||||
    required this.muted,
 | 
					 | 
				
			||||||
    required this.onMuteToggle,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
 | 
					  State<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
					class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
				
			||||||
  bool _showSeekIndicator = false;
 | 
					  final SettingsController _settingsController = Get.find();
 | 
				
			||||||
  bool _isRewinding = false;
 | 
					 | 
				
			||||||
  Timer? _hideTimer;
 | 
					  Timer? _hideTimer;
 | 
				
			||||||
 | 
					  bool _controlsVisible = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isScrubbing = false;
 | 
				
			||||||
 | 
					  Duration _scrubbingStartPosition = Duration.zero;
 | 
				
			||||||
 | 
					  double _scrubbingStartDx = 0.0;
 | 
				
			||||||
 | 
					  Duration _scrubbingSeekPosition = Duration.zero;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    widget.controller.addListener(_listener);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _hideTimer?.cancel();
 | 
					    _hideTimer?.cancel();
 | 
				
			||||||
 | 
					    widget.controller.removeListener(_listener);
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _handleDoubleTap(TapDownDetails details) {
 | 
					  void _listener() {
 | 
				
			||||||
    final double screenWidth = MediaQuery.of(context).size.width;
 | 
					    if (mounted) {
 | 
				
			||||||
    final bool isRewind = details.globalPosition.dx < screenWidth / 2;
 | 
					      setState(() {});
 | 
				
			||||||
    widget.onOverlayTap();
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Future(() {
 | 
					 | 
				
			||||||
      if (isRewind) {
 | 
					 | 
				
			||||||
        final Duration newPosition =
 | 
					 | 
				
			||||||
            widget.controller.value.position - const Duration(seconds: 10);
 | 
					 | 
				
			||||||
        widget.controller.seekTo(
 | 
					 | 
				
			||||||
          newPosition < Duration.zero ? Duration.zero : newPosition,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        final Duration newPosition =
 | 
					 | 
				
			||||||
            widget.controller.value.position + const Duration(seconds: 10);
 | 
					 | 
				
			||||||
        final Duration duration = widget.controller.value.duration;
 | 
					 | 
				
			||||||
        widget.controller.seekTo(
 | 
					 | 
				
			||||||
          newPosition > duration ? duration : newPosition,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _startHideTimer() {
 | 
				
			||||||
    _hideTimer?.cancel();
 | 
					    _hideTimer?.cancel();
 | 
				
			||||||
    setState(() {
 | 
					    _hideTimer = Timer(const Duration(seconds: 5), () {
 | 
				
			||||||
      _showSeekIndicator = true;
 | 
					      if (mounted) {
 | 
				
			||||||
      _isRewinding = isRewind;
 | 
					        setState(() => _controlsVisible = false);
 | 
				
			||||||
    });
 | 
					      }
 | 
				
			||||||
    _hideTimer = Timer(const Duration(milliseconds: 500), () {
 | 
					 | 
				
			||||||
      setState(() => _showSeekIndicator = false);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  void _toggleControlsVisibility() {
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					    setState(() => _controlsVisible = !_controlsVisible);
 | 
				
			||||||
    return Stack(
 | 
					    if (_controlsVisible) {
 | 
				
			||||||
      alignment: Alignment.center,
 | 
					      _startHideTimer();
 | 
				
			||||||
      children: [
 | 
					    }
 | 
				
			||||||
        GestureDetector(
 | 
					  }
 | 
				
			||||||
          onTap: widget.onOverlayTap,
 | 
					
 | 
				
			||||||
          onDoubleTapDown: _handleDoubleTap,
 | 
					  void _handlePlayPause() {
 | 
				
			||||||
          child: Container(color: Colors.transparent),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        AnimatedOpacity(
 | 
					 | 
				
			||||||
          opacity: _showSeekIndicator ? 1.0 : 0.0,
 | 
					 | 
				
			||||||
          duration: const Duration(milliseconds: 200),
 | 
					 | 
				
			||||||
          child: Align(
 | 
					 | 
				
			||||||
            alignment: _isRewinding
 | 
					 | 
				
			||||||
                ? Alignment.centerLeft
 | 
					 | 
				
			||||||
                : Alignment.centerRight,
 | 
					 | 
				
			||||||
            child: Padding(
 | 
					 | 
				
			||||||
              padding: const EdgeInsets.symmetric(horizontal: 40.0),
 | 
					 | 
				
			||||||
              child: Icon(
 | 
					 | 
				
			||||||
                _isRewinding
 | 
					 | 
				
			||||||
                    ? Icons.fast_rewind_rounded
 | 
					 | 
				
			||||||
                    : Icons.fast_forward_rounded,
 | 
					 | 
				
			||||||
                color: Colors.white70,
 | 
					 | 
				
			||||||
                size: 60,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        IconButton(
 | 
					 | 
				
			||||||
          icon: Icon(
 | 
					 | 
				
			||||||
            widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
					 | 
				
			||||||
            size: 64,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onPressed: () {
 | 
					 | 
				
			||||||
            widget.onOverlayTap();
 | 
					 | 
				
			||||||
    widget.controller.value.isPlaying
 | 
					    widget.controller.value.isPlaying
 | 
				
			||||||
        ? widget.controller.pause()
 | 
					        ? widget.controller.pause()
 | 
				
			||||||
        : widget.controller.play();
 | 
					        : widget.controller.play();
 | 
				
			||||||
          },
 | 
					    _startHideTimer();
 | 
				
			||||||
        ),
 | 
					  }
 | 
				
			||||||
        Positioned(
 | 
					
 | 
				
			||||||
          right: 12,
 | 
					  void _onHorizontalDragStart(DragStartDetails details) {
 | 
				
			||||||
          bottom: 12,
 | 
					    if (!widget.controller.value.isInitialized || !_controlsVisible) return;
 | 
				
			||||||
          child: IconButton(
 | 
					
 | 
				
			||||||
            icon: Icon(
 | 
					    setState(() {
 | 
				
			||||||
              widget.muted ? Icons.volume_off : Icons.volume_up,
 | 
					      _isScrubbing = true;
 | 
				
			||||||
              size: 16,
 | 
					      _scrubbingStartPosition = widget.controller.value.position;
 | 
				
			||||||
            ),
 | 
					      _scrubbingStartDx = details.globalPosition.dx;
 | 
				
			||||||
            onPressed: () {
 | 
					      _scrubbingSeekPosition = widget.controller.value.position;
 | 
				
			||||||
              widget.onOverlayTap();
 | 
					    });
 | 
				
			||||||
              widget.onMuteToggle();
 | 
					    _hideTimer?.cancel();
 | 
				
			||||||
            },
 | 
					  }
 | 
				
			||||||
          ),
 | 
					
 | 
				
			||||||
        ),
 | 
					  void _onHorizontalDragUpdate(DragUpdateDetails details) {
 | 
				
			||||||
        Align(
 | 
					    if (!_isScrubbing) return;
 | 
				
			||||||
          alignment: Alignment.bottomCenter,
 | 
					
 | 
				
			||||||
          child: Padding(
 | 
					    final double delta = details.globalPosition.dx - _scrubbingStartDx;
 | 
				
			||||||
            padding: const EdgeInsets.only(bottom: 0),
 | 
					    final int seekMillis =
 | 
				
			||||||
            child: LayoutBuilder(
 | 
					        _scrubbingStartPosition.inMilliseconds + (delta * 300).toInt();
 | 
				
			||||||
              builder: (context, constraints) {
 | 
					
 | 
				
			||||||
                return Stack(
 | 
					    setState(() {
 | 
				
			||||||
                  clipBehavior: Clip.none,
 | 
					      final Duration duration = widget.controller.value.duration;
 | 
				
			||||||
                  children: [
 | 
					      final Duration seekDuration = Duration(milliseconds: seekMillis);
 | 
				
			||||||
                    Positioned(
 | 
					      final Duration clampedSeekDuration = seekDuration < Duration.zero
 | 
				
			||||||
                      left: 10,
 | 
					          ? Duration.zero
 | 
				
			||||||
                      bottom: 12,
 | 
					          : (seekDuration > duration ? duration : seekDuration);
 | 
				
			||||||
                      child: Text(
 | 
					      _scrubbingSeekPosition = clampedSeekDuration;
 | 
				
			||||||
                        '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
 | 
					    });
 | 
				
			||||||
                        style: const TextStyle(
 | 
					  }
 | 
				
			||||||
                          color: Colors.white,
 | 
					
 | 
				
			||||||
                          fontSize: 12,
 | 
					  void _onHorizontalDragEnd(DragEndDetails details) {
 | 
				
			||||||
                        ),
 | 
					    if (!_isScrubbing) return;
 | 
				
			||||||
                      ),
 | 
					
 | 
				
			||||||
                    ),
 | 
					    widget.controller.seekTo(_scrubbingSeekPosition);
 | 
				
			||||||
                    Listener(
 | 
					    setState(() => _isScrubbing = false);
 | 
				
			||||||
                      onPointerDown: (_) {
 | 
					    _startHideTimer();
 | 
				
			||||||
                        widget.onOverlayTap();
 | 
					  }
 | 
				
			||||||
                      },
 | 
					
 | 
				
			||||||
                      child: VideoProgressIndicator(
 | 
					  void _onHorizontalDragCancel() {
 | 
				
			||||||
                        widget.controller,
 | 
					    if (!_isScrubbing) return;
 | 
				
			||||||
                        allowScrubbing: true,
 | 
					    setState(() => _isScrubbing = false);
 | 
				
			||||||
                        padding: const EdgeInsets.only(top: 25.0),
 | 
					    _startHideTimer();
 | 
				
			||||||
                        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),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    if (widget.controller.value.duration.inMilliseconds > 0)
 | 
					 | 
				
			||||||
                      Positioned(
 | 
					 | 
				
			||||||
                        left:
 | 
					 | 
				
			||||||
                            (widget.controller.value.position.inMilliseconds /
 | 
					 | 
				
			||||||
                                    widget
 | 
					 | 
				
			||||||
                                        .controller
 | 
					 | 
				
			||||||
                                        .value
 | 
					 | 
				
			||||||
                                        .duration
 | 
					 | 
				
			||||||
                                        .inMilliseconds) *
 | 
					 | 
				
			||||||
                                constraints.maxWidth -
 | 
					 | 
				
			||||||
                            6,
 | 
					 | 
				
			||||||
                        bottom: -4,
 | 
					 | 
				
			||||||
                        child: Container(
 | 
					 | 
				
			||||||
                          width: 12,
 | 
					 | 
				
			||||||
                          height: 12,
 | 
					 | 
				
			||||||
                          decoration: BoxDecoration(
 | 
					 | 
				
			||||||
                            shape: BoxShape.circle,
 | 
					 | 
				
			||||||
                            color: Theme.of(context).colorScheme.primary,
 | 
					 | 
				
			||||||
                            border: Border.all(
 | 
					 | 
				
			||||||
                              color: Theme.of(context).colorScheme.primary,
 | 
					 | 
				
			||||||
                              width: 2,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String _formatDuration(Duration? duration) {
 | 
					  String _formatDuration(Duration? duration) {
 | 
				
			||||||
@@ -199,4 +113,149 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
				
			|||||||
    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
					    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
				
			||||||
    return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
 | 
					    return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return GestureDetector(
 | 
				
			||||||
 | 
					      behavior: HitTestBehavior.opaque,
 | 
				
			||||||
 | 
					      onTap: _toggleControlsVisibility,
 | 
				
			||||||
 | 
					      onHorizontalDragStart: _controlsVisible ? _onHorizontalDragStart : null,
 | 
				
			||||||
 | 
					      onHorizontalDragUpdate: _controlsVisible ? _onHorizontalDragUpdate : null,
 | 
				
			||||||
 | 
					      onHorizontalDragEnd: _controlsVisible ? _onHorizontalDragEnd : null,
 | 
				
			||||||
 | 
					      onHorizontalDragCancel: _controlsVisible ? _onHorizontalDragCancel : null,
 | 
				
			||||||
 | 
					      child: Stack(
 | 
				
			||||||
 | 
					        alignment: Alignment.center,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          AnimatedContainer(
 | 
				
			||||||
 | 
					            duration: const Duration(milliseconds: 300),
 | 
				
			||||||
 | 
					            color: _controlsVisible && !_isScrubbing
 | 
				
			||||||
 | 
					                ? Colors.black.withValues(alpha: 0.5)
 | 
				
			||||||
 | 
					                : Colors.transparent,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          AnimatedOpacity(
 | 
				
			||||||
 | 
					            opacity: _isScrubbing ? 1.0 : 0.0,
 | 
				
			||||||
 | 
					            duration: const Duration(milliseconds: 200),
 | 
				
			||||||
 | 
					            child: _buildScrubbingIndicator(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          AnimatedOpacity(
 | 
				
			||||||
 | 
					            opacity: _controlsVisible && !_isScrubbing ? 1.0 : 0.0,
 | 
				
			||||||
 | 
					            duration: const Duration(milliseconds: 300),
 | 
				
			||||||
 | 
					            child: IgnorePointer(
 | 
				
			||||||
 | 
					              ignoring: !_controlsVisible,
 | 
				
			||||||
 | 
					              child: Stack(
 | 
				
			||||||
 | 
					                alignment: Alignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  IconButton(
 | 
				
			||||||
 | 
					                    icon: Icon(
 | 
				
			||||||
 | 
					                      widget.controller.value.isPlaying
 | 
				
			||||||
 | 
					                          ? Icons.pause
 | 
				
			||||||
 | 
					                          : Icons.play_arrow,
 | 
				
			||||||
 | 
					                      size: 64,
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onPressed: _handlePlayPause,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  Align(
 | 
				
			||||||
 | 
					                    alignment: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					                    child: _buildBottomBar(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildScrubbingIndicator() {
 | 
				
			||||||
 | 
					    final Duration positionChange =
 | 
				
			||||||
 | 
					        _scrubbingSeekPosition - _scrubbingStartPosition;
 | 
				
			||||||
 | 
					    final String changeSign = positionChange.isNegative ? '-' : '+';
 | 
				
			||||||
 | 
					    final String changeText = _formatDuration(positionChange.abs());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					      decoration: BoxDecoration(
 | 
				
			||||||
 | 
					        color: Colors.black.withValues(alpha: 0.7),
 | 
				
			||||||
 | 
					        borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            _formatDuration(_scrubbingSeekPosition),
 | 
				
			||||||
 | 
					            style: const TextStyle(
 | 
				
			||||||
 | 
					              color: Colors.white,
 | 
				
			||||||
 | 
					              fontSize: 22,
 | 
				
			||||||
 | 
					              fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            '[$changeSign$changeText]',
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              color: Colors.white.withValues(alpha: 0.8),
 | 
				
			||||||
 | 
					              fontSize: 16,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildBottomBar() {
 | 
				
			||||||
 | 
					    return Padding(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            _formatDuration(widget.controller.value.position),
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					              fontSize: 12,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
 | 
					              child: VideoProgressIndicator(
 | 
				
			||||||
 | 
					                widget.controller,
 | 
				
			||||||
 | 
					                allowScrubbing: true,
 | 
				
			||||||
 | 
					                colors: VideoProgressColors(
 | 
				
			||||||
 | 
					                  playedColor: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					                  backgroundColor: Colors.white.withValues(alpha: 0.3),
 | 
				
			||||||
 | 
					                  bufferedColor: Colors.white.withValues(alpha: 0.6),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            _formatDuration(widget.controller.value.duration),
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					              fontSize: 12,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const SizedBox(width: 8),
 | 
				
			||||||
 | 
					          Obx(
 | 
				
			||||||
 | 
					            () => IconButton(
 | 
				
			||||||
 | 
					              icon: Icon(
 | 
				
			||||||
 | 
					                _settingsController.muted.value
 | 
				
			||||||
 | 
					                    ? Icons.volume_off
 | 
				
			||||||
 | 
					                    : Icons.volume_up,
 | 
				
			||||||
 | 
					                color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					                size: 20,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onPressed: () {
 | 
				
			||||||
 | 
					                _settingsController.toggleMuted();
 | 
				
			||||||
 | 
					                _startHideTimer();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              constraints: const BoxConstraints(),
 | 
				
			||||||
 | 
					              padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ class VideoWidget extends StatefulWidget {
 | 
				
			|||||||
  final bool isActive;
 | 
					  final bool isActive;
 | 
				
			||||||
  final bool fullScreen;
 | 
					  final bool fullScreen;
 | 
				
			||||||
  final VoidCallback? onInitialized;
 | 
					  final VoidCallback? onInitialized;
 | 
				
			||||||
 | 
					  final Duration? initialPosition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const VideoWidget({
 | 
					  const VideoWidget({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -23,21 +24,18 @@ class VideoWidget extends StatefulWidget {
 | 
				
			|||||||
    required this.isActive,
 | 
					    required this.isActive,
 | 
				
			||||||
    this.fullScreen = false,
 | 
					    this.fullScreen = false,
 | 
				
			||||||
    this.onInitialized,
 | 
					    this.onInitialized,
 | 
				
			||||||
 | 
					    this.initialPosition,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<VideoWidget> createState() => _VideoWidgetState();
 | 
					  State<VideoWidget> createState() => VideoWidgetState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _VideoWidgetState extends State<VideoWidget> {
 | 
					class VideoWidgetState extends State<VideoWidget> {
 | 
				
			||||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
					  final MediaController mediaController = Get.find<MediaController>();
 | 
				
			||||||
  final SettingsController settingsController = Get.find<SettingsController>();
 | 
					  final SettingsController settingsController = Get.find<SettingsController>();
 | 
				
			||||||
  late CachedVideoPlayerPlusController videoController;
 | 
					  late CachedVideoPlayerPlusController videoController;
 | 
				
			||||||
  late Worker _muteWorker;
 | 
					  late Worker _muteWorker;
 | 
				
			||||||
  late Worker _timerResetWorker;
 | 
					 | 
				
			||||||
  late Worker _hideControlsWorker;
 | 
					 | 
				
			||||||
  bool _showControls = false;
 | 
					 | 
				
			||||||
  Timer? _hideControlsTimer;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
@@ -48,22 +46,6 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
        videoController.setVolume(muted ? 0.0 : 1.0);
 | 
					        videoController.setVolume(muted ? 0.0 : 1.0);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    _timerResetWorker = ever(settingsController.videoControlsTimerNotifier, (
 | 
					 | 
				
			||||||
      _,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      if (widget.isActive && mounted) {
 | 
					 | 
				
			||||||
        if (!_showControls) {
 | 
					 | 
				
			||||||
          setState(() => _showControls = true);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        _startHideControlsTimer();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    _hideControlsWorker = ever(settingsController.hideControlsNotifier, (_) {
 | 
					 | 
				
			||||||
      if (mounted && _showControls) {
 | 
					 | 
				
			||||||
        setState(() => _showControls = false);
 | 
					 | 
				
			||||||
        _hideControlsTimer?.cancel();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _initController() async {
 | 
					  Future<void> _initController() async {
 | 
				
			||||||
@@ -72,8 +54,14 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
 | 
					      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    await videoController.initialize();
 | 
					    await videoController.initialize();
 | 
				
			||||||
    widget.onInitialized?.call();
 | 
					 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (widget.initialPosition != null) {
 | 
				
			||||||
 | 
					      await videoController.seekTo(widget.initialPosition!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    widget.onInitialized?.call();
 | 
				
			||||||
    videoController.setLooping(true);
 | 
					    videoController.setLooping(true);
 | 
				
			||||||
    videoController.setVolume(settingsController.muted.value ? 0.0 : 1.0);
 | 
					    videoController.setVolume(settingsController.muted.value ? 0.0 : 1.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -93,6 +81,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.isActive != oldWidget.isActive) {
 | 
					    if (widget.isActive != oldWidget.isActive) {
 | 
				
			||||||
 | 
					      if (videoController.value.isInitialized) {
 | 
				
			||||||
        if (widget.isActive) {
 | 
					        if (widget.isActive) {
 | 
				
			||||||
          videoController.play();
 | 
					          videoController.play();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -100,45 +89,17 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _muteWorker.dispose();
 | 
					    _muteWorker.dispose();
 | 
				
			||||||
    _timerResetWorker.dispose();
 | 
					 | 
				
			||||||
    _hideControlsWorker.dispose();
 | 
					 | 
				
			||||||
    videoController.dispose();
 | 
					    videoController.dispose();
 | 
				
			||||||
    _hideControlsTimer?.cancel();
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _startHideControlsTimer() {
 | 
					 | 
				
			||||||
    _hideControlsTimer?.cancel();
 | 
					 | 
				
			||||||
    _hideControlsTimer = Timer(const Duration(seconds: 3), () {
 | 
					 | 
				
			||||||
      if (mounted) {
 | 
					 | 
				
			||||||
        setState(() => _showControls = false);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _onTap({bool ctrlButton = false}) {
 | 
					 | 
				
			||||||
    if (ctrlButton) {
 | 
					 | 
				
			||||||
      _startHideControlsTimer();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final bool newShowState = !_showControls;
 | 
					 | 
				
			||||||
    setState(() => _showControls = newShowState);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (newShowState) {
 | 
					 | 
				
			||||||
      _startHideControlsTimer();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      _hideControlsTimer?.cancel();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final bool muted = settingsController.muted.value;
 | 
					 | 
				
			||||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
					    bool isAudio = widget.details.mime.startsWith('audio');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget mediaContent;
 | 
					    Widget mediaContent;
 | 
				
			||||||
@@ -165,26 +126,13 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
      child: Stack(
 | 
					      child: Stack(
 | 
				
			||||||
        alignment: Alignment.center,
 | 
					        alignment: Alignment.center,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          GestureDetector(onTap: _onTap, child: mediaContent),
 | 
					          mediaContent,
 | 
				
			||||||
          AnimatedBuilder(
 | 
					          AnimatedBuilder(
 | 
				
			||||||
            animation: videoController,
 | 
					            animation: videoController,
 | 
				
			||||||
            builder: (context, child) {
 | 
					            builder: (context, child) {
 | 
				
			||||||
              if (videoController.value.isInitialized && _showControls) {
 | 
					              if (videoController.value.isInitialized) {
 | 
				
			||||||
                return Positioned.fill(
 | 
					                return Positioned.fill(
 | 
				
			||||||
                  child: GestureDetector(
 | 
					                  child: VideoControlsOverlay(controller: videoController),
 | 
				
			||||||
                    onTap: _onTap,
 | 
					 | 
				
			||||||
                    child: Container(
 | 
					 | 
				
			||||||
                      color: Colors.black.withValues(alpha: 0.5),
 | 
					 | 
				
			||||||
                      child: VideoControlsOverlay(
 | 
					 | 
				
			||||||
                        controller: videoController,
 | 
					 | 
				
			||||||
                        onOverlayTap: () => _onTap(ctrlButton: true),
 | 
					 | 
				
			||||||
                        muted: muted,
 | 
					 | 
				
			||||||
                        onMuteToggle: () {
 | 
					 | 
				
			||||||
                          settingsController.toggleMuted();
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              return const SizedBox.shrink();
 | 
					              return const SizedBox.shrink();
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user