import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; 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:timeago/timeago.dart' as timeago; 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/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 } class MediaDetailScreen extends StatefulWidget { final int initialId; const MediaDetailScreen({super.key, required this.initialId}); @override State createState() => _MediaDetailScreenState(); } class _MediaDetailScreenState extends State { PageController? _pageController; final MediaController mediaController = Get.find(); final SettingsController settingsController = Get.find(); final AuthController authController = Get.find(); final RxInt _currentIndex = 0.obs; final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit'); final Map _refreshControllers = {}; final Map> _videoWidgetKeys = {}; bool _isLoading = true; bool _itemNotFound = false; final RxSet _readyItemIds = {}.obs; final Rxn _animatingFavoriteId = Rxn(); final List> _shareMenuItems = const [ PopupMenuItem( value: ShareAction.media, child: ListTile(leading: Icon(Icons.image), title: Text('Als Datei')), ), PopupMenuItem( value: ShareAction.directLink, child: ListTile(leading: Icon(Icons.link), title: Text('Link zur Datei')), ), PopupMenuItem( value: ShareAction.postLink, child: ListTile( leading: Icon(Icons.article), title: Text('Link zum f0ck'), ), ), ]; @override void initState() { super.initState(); timeago.setLocaleMessages('de', timeago.DeMessages()); _loadInitialItem(); } Future _loadInitialItem() async { int initialIndex = mediaController.items.indexWhere( (item) => item.id == widget.initialId, ); if (initialIndex < 0) { await mediaController.fetchInitial(id: widget.initialId + 20); initialIndex = mediaController.items.indexWhere( (item) => item.id == widget.initialId, ); } if (initialIndex < 0) { if (mounted) { setState(() { _itemNotFound = true; _isLoading = false; }); } return; } if (mounted) { _currentIndex.value = initialIndex; _pageController = PageController(initialPage: initialIndex); if (mediaController.items[initialIndex].mime.startsWith('image/')) { _readyItemIds.add(mediaController.items[initialIndex].id); } setState(() => _isLoading = false); } } void _showMsg(String message) { if (!mounted) return; Get ..closeAllSnackbars() ..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM); } Future _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; final MediaItem item = mediaController.items[idx]; if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) { _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 && !mediaController.loading.value && !mediaController.atEnd.value) { mediaController.fetchMore(); } else if (idx <= 1 && !mediaController.loading.value && !mediaController.atStart.value) { mediaController.fetchNewer(); } } Future _downloadMedia(MediaItem item) async { try { final File file = await DefaultCacheManager().getSingleFile( item.mediaUrl, ); final bool? success = await _mediaSaverChannel.invokeMethod( 'saveFile', {'filePath': file.path, 'fileName': item.dest}, ); success == true ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.') : _showMsg('${item.dest} konnte nicht heruntergeladen werden.'); } catch (e) { _showMsg('Fehler beim Download: $e'); } } Future _handleShareAction(ShareAction value, MediaItem item) async { try { if (value == ShareAction.media) { final File file = await DefaultCacheManager().getSingleFile( item.mediaUrl, ); final Uint8List bytes = await file.readAsBytes(); final ShareParams params = ShareParams( files: [XFile.fromData(bytes, mimeType: item.mime)], ); await SharePlus.instance.share(params); return; } final String textToShare; switch (value) { case ShareAction.directLink: textToShare = item.mediaUrl; break; case ShareAction.postLink: textToShare = item.postUrl; break; case ShareAction.media: return; } await SharePlus.instance.share(ShareParams(text: textToShare)); } catch (e) { _showMsg('Fehler beim Teilen: $e'); } } Future _handleFullScreen(MediaItem currentItem) async { if (currentItem.mime.startsWith('image')) { Get.to( () => FullScreenMediaView(item: currentItem), fullscreenDialog: true, ); return; } final GlobalKey? 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( () => FullScreenMediaView(item: currentItem, initialPosition: position), fullscreenDialog: true, ); if (mounted && videoState.mounted) { if (newPosition != null) { await videoState.videoController.seekTo(newPosition); } await videoState.videoController.play(); } } @override void dispose() { _pageController?.dispose(); for (PullexRefreshController controller in _refreshControllers.values) { controller.dispose(); } super.dispose(); } Future _handleFavoriteToggle(MediaItem item, bool isFavorite) async { if (!authController.isLoggedIn) return; HapticFeedback.lightImpact(); final List? 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(); } _animatingFavoriteId.value = item.id; Future.delayed(const Duration(milliseconds: 700), () { if (_animatingFavoriteId.value == item.id) { _animatingFavoriteId.value = null; } }); } Widget _buildMedia(MediaItem item, bool isActive) { Widget mediaWidget; final bool isFavorite = item.favorites?.any((f) => f.userId == authController.user.value?.id) ?? false; if (item.mime.startsWith('image/')) { mediaWidget = CachedNetworkImage( imageUrl: item.mediaUrl, fit: BoxFit.contain, placeholder: (context, url) => const Center(child: CircularProgressIndicator()), errorWidget: (c, e, s) => const Icon(Icons.broken_image, size: 100), ); } else if (item.mime.startsWith('video/') || item.mime.startsWith('audio/')) { final GlobalKey key = _videoWidgetKeys.putIfAbsent( item.id, () => GlobalKey(), ); mediaWidget = VideoWidget( key: key, details: item, isActive: isActive, onInitialized: () { if (mounted && !_readyItemIds.contains(item.id)) { _readyItemIds.add(item.id); } }, ); } else { mediaWidget = const Icon(Icons.help_outline, size: 100); } return Hero( tag: 'media_${item.id}', child: Stack( alignment: Alignment.center, children: [ GestureDetector( onDoubleTap: () => _handleFavoriteToggle(item, isFavorite), child: mediaWidget, ), Obx(() { final showAnimation = _animatingFavoriteId.value == item.id; return AnimatedOpacity( opacity: showAnimation ? 1.0 : 0.0, duration: const Duration(milliseconds: 200), child: AnimatedScale( scale: showAnimation ? 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, ), ), ); }), ], ), ); } PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( title: Obx(() { if (_isLoading) { return Text('Lade f0ck #${widget.initialId}...'); } if (_itemNotFound || mediaController.items.isEmpty || _currentIndex.value >= mediaController.items.length) { return const Text('Fehler'); } final MediaItem currentItem = mediaController.items[_currentIndex.value]; return Text('f0ck #${currentItem.id}'); }), actions: [ Obx(() { final bool showActions = !_isLoading && !_itemNotFound && mediaController.items.isNotEmpty && _currentIndex.value < mediaController.items.length; if (!showActions) { return const SizedBox.shrink(); } final MediaItem currentItem = mediaController.items[_currentIndex.value]; return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.fullscreen), onPressed: () => _handleFullScreen(currentItem), ), IconButton( icon: const Icon(Icons.download), onPressed: () async => await _downloadMedia(currentItem), ), PopupMenuButton( 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(), ), ), ], ); } Widget _buildBody(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_itemNotFound) { return const Center(child: Text('f0ck nicht gefunden.')); } return Obx(() { if (mediaController.items.isEmpty) { return const Center(child: Text('Keine Items zum Anzeigen.')); } return PageView.builder( controller: _pageController!, itemCount: mediaController.items.length, onPageChanged: _onPageChanged, itemBuilder: (context, index) { if (index >= mediaController.items.length) { return const SizedBox.shrink(); } final MediaItem item = mediaController.items[index]; final PullexRefreshController refreshController = _refreshControllers .putIfAbsent(item.id, () => PullexRefreshController()); return Obx(() { final bool isReady = _readyItemIds.contains(item.id); return PullexRefresh( onRefresh: () => _onRefresh(item.id, refreshController), header: const WaterDropHeader(), controller: refreshController, child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: AnimatedBuilder( animation: _pageController!, builder: (context, child) { return buildAnimatedTransition( context: context, pageController: _pageController!, index: index, child: child!, ); }, child: Obx( () => _buildMedia(item, index == _currentIndex.value), ), ), ), if (isReady) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ TagSection(tags: item.tags ?? []), Obx(() { if (!authController.isLoggedIn) { return const SizedBox.shrink(); } final TextStyle? infoTextStyle = Theme.of( context, ).textTheme.bodySmall; return Padding( padding: const EdgeInsets.only(top: 24.0), child: Column( children: [ FavoriteSection(item: item, index: index), const SizedBox(height: 16), Text( "Dateigröße: ${(item.size / 1024).toStringAsFixed(1)} KB", style: infoTextStyle, ), Text( "Typ: ${item.mime}", style: infoTextStyle, ), Text( "ID: ${item.id}", style: infoTextStyle, ), Text( "Hochgeladen: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(item.stamp * 1000), locale: 'de')}", style: infoTextStyle, ), ], ), ); }), ], ), ), ), const SliverToBoxAdapter( child: SafeArea(child: SizedBox.shrink()), ), ], ), ); }); }, ); }); } @override Widget build(BuildContext context) { return Scaffold( endDrawer: const EndDrawer(), endDrawerEnableOpenDragGesture: settingsController.drawerSwipeEnabled.value, appBar: _buildAppBar(context), body: _buildBody(context), persistentFooterButtons: mediaController.tag.value != null ? [TagFooter()] : null, ); } }