This commit is contained in:
		@@ -50,7 +50,7 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
 | 
			
		||||
                          const Icon(Icons.error),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                : SizedBox.expand(
 | 
			
		||||
                : Center(
 | 
			
		||||
                    child: VideoWidget(
 | 
			
		||||
                      details: widget.item,
 | 
			
		||||
                      isActive: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,19 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:share_plus/share_plus.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/actiontag.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/favorite_avatars.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/favoritesection.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/fullscreen.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
 | 
			
		||||
enum ShareAction { media, directLink, postLink }
 | 
			
		||||
 | 
			
		||||
class MediaDetailScreen extends StatefulWidget {
 | 
			
		||||
  final int initialId;
 | 
			
		||||
  const MediaDetailScreen({super.key, required this.initialId});
 | 
			
		||||
@@ -26,19 +29,70 @@ class MediaDetailScreen extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  late PageController _pageController;
 | 
			
		||||
  PageController? _pageController;
 | 
			
		||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
			
		||||
  final AuthController authController = Get.find<AuthController>();
 | 
			
		||||
  int? _currentIndex;
 | 
			
		||||
  final _currentIndex = 0.obs;
 | 
			
		||||
  final _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
			
		||||
 | 
			
		||||
  bool _isLoading = true;
 | 
			
		||||
  bool _itemNotFound = false;
 | 
			
		||||
  final Set<int> _readyItemIds = {};
 | 
			
		||||
 | 
			
		||||
  final List<PopupMenuEntry<ShareAction>> _shareMenuItems = const [
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.media,
 | 
			
		||||
      child: ListTile(leading: Icon(Icons.image), title: Text('Als Datei')),
 | 
			
		||||
    ),
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.directLink,
 | 
			
		||||
      child: ListTile(leading: Icon(Icons.link), title: Text('Link zur Datei')),
 | 
			
		||||
    ),
 | 
			
		||||
    PopupMenuItem(
 | 
			
		||||
      value: ShareAction.postLink,
 | 
			
		||||
      child: ListTile(
 | 
			
		||||
        leading: Icon(Icons.article),
 | 
			
		||||
        title: Text('Link zum f0ck'),
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    final int idx = mediaController.items.indexWhere(
 | 
			
		||||
    _loadInitialItem();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _loadInitialItem() async {
 | 
			
		||||
    int initialIndex = mediaController.items.indexWhere(
 | 
			
		||||
      (item) => item.id == widget.initialId,
 | 
			
		||||
    );
 | 
			
		||||
    _currentIndex = idx >= 0 ? idx : 0;
 | 
			
		||||
    _pageController = PageController(initialPage: _currentIndex!);
 | 
			
		||||
 | 
			
		||||
    if (initialIndex < 0) {
 | 
			
		||||
      await mediaController.fetchInitial(id: widget.initialId + 20);
 | 
			
		||||
      initialIndex = mediaController.items.indexWhere(
 | 
			
		||||
        (item) => item.id == widget.initialId,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (initialIndex < 0) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          _itemNotFound = true;
 | 
			
		||||
          _isLoading = false;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mounted) {
 | 
			
		||||
      _currentIndex.value = initialIndex;
 | 
			
		||||
      _pageController = PageController(initialPage: initialIndex);
 | 
			
		||||
      if (mediaController.items[initialIndex].mime.startsWith('image/')) {
 | 
			
		||||
        _readyItemIds.add(mediaController.items[initialIndex].id);
 | 
			
		||||
      }
 | 
			
		||||
      setState(() => _isLoading = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showMsg(String message) {
 | 
			
		||||
@@ -49,15 +103,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onPageChanged(int idx) {
 | 
			
		||||
    if (idx != _currentIndex) {
 | 
			
		||||
      setState(() => _currentIndex = idx);
 | 
			
		||||
    if (idx != _currentIndex.value) {
 | 
			
		||||
      _currentIndex.value = idx;
 | 
			
		||||
      final item = mediaController.items[idx];
 | 
			
		||||
      if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
 | 
			
		||||
        setState(() => _readyItemIds.add(item.id));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (idx >= mediaController.items.length - 2 &&
 | 
			
		||||
        !mediaController.loading.value &&
 | 
			
		||||
        !mediaController.atEnd.value) {
 | 
			
		||||
      mediaController.fetchMore();
 | 
			
		||||
    }
 | 
			
		||||
    if (idx <= 1 &&
 | 
			
		||||
    } else if (idx <= 1 &&
 | 
			
		||||
        !mediaController.loading.value &&
 | 
			
		||||
        !mediaController.atStart.value) {
 | 
			
		||||
      mediaController.fetchNewer();
 | 
			
		||||
@@ -65,22 +122,58 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _downloadMedia(MediaItem item) async {
 | 
			
		||||
    final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
 | 
			
		||||
    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
			
		||||
    try {
 | 
			
		||||
      final File file = await DefaultCacheManager().getSingleFile(
 | 
			
		||||
        item.mediaUrl,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
			
		||||
      'filePath': file.path,
 | 
			
		||||
      'fileName': item.dest,
 | 
			
		||||
    });
 | 
			
		||||
      final bool? success = await _mediaSaverChannel.invokeMethod<bool>(
 | 
			
		||||
        'saveFile',
 | 
			
		||||
        {'filePath': file.path, 'fileName': item.dest},
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    success == true
 | 
			
		||||
        ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
			
		||||
        : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
			
		||||
      success == true
 | 
			
		||||
          ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
			
		||||
          : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showMsg('Fehler beim Download: $e');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _handleShareAction(ShareAction value, MediaItem item) async {
 | 
			
		||||
    try {
 | 
			
		||||
      if (value == ShareAction.media) {
 | 
			
		||||
        final File file = await DefaultCacheManager().getSingleFile(
 | 
			
		||||
          item.mediaUrl,
 | 
			
		||||
        );
 | 
			
		||||
        final Uint8List bytes = await file.readAsBytes();
 | 
			
		||||
        final params = ShareParams(
 | 
			
		||||
          files: [XFile.fromData(bytes, mimeType: item.mime)],
 | 
			
		||||
        );
 | 
			
		||||
        await SharePlus.instance.share(params);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      final String textToShare;
 | 
			
		||||
      switch (value) {
 | 
			
		||||
        case ShareAction.directLink:
 | 
			
		||||
          textToShare = item.mediaUrl;
 | 
			
		||||
          break;
 | 
			
		||||
        case ShareAction.postLink:
 | 
			
		||||
          textToShare = item.postUrl;
 | 
			
		||||
          break;
 | 
			
		||||
        case ShareAction.media:
 | 
			
		||||
          return;
 | 
			
		||||
      }
 | 
			
		||||
      await SharePlus.instance.share(ShareParams(text: textToShare));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showMsg('Fehler beim Teilen: $e');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _pageController.dispose();
 | 
			
		||||
    _pageController?.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +186,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    } else if (item.mime.startsWith('video/') ||
 | 
			
		||||
        item.mime.startsWith('audio/')) {
 | 
			
		||||
      return VideoWidget(details: item, isActive: isActive);
 | 
			
		||||
      return VideoWidget(
 | 
			
		||||
        details: item,
 | 
			
		||||
        isActive: isActive,
 | 
			
		||||
        onInitialized: () {
 | 
			
		||||
          if (mounted && !_readyItemIds.contains(item.id)) {
 | 
			
		||||
            setState(() => _readyItemIds.add(item.id));
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return const Icon(Icons.help_outline, size: 100);
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,209 +202,134 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (_isLoading) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(title: Text('Lade f0ck #${widget.initialId}...')),
 | 
			
		||||
        body: const Center(child: CircularProgressIndicator()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (_itemNotFound) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(title: const Text('Fehler')),
 | 
			
		||||
        body: const Center(child: Text('f0ck nicht gefunden.')),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Obx(
 | 
			
		||||
      () => PageView.builder(
 | 
			
		||||
        controller: _pageController,
 | 
			
		||||
        controller: _pageController!,
 | 
			
		||||
        itemCount: mediaController.items.length,
 | 
			
		||||
        onPageChanged: _onPageChanged,
 | 
			
		||||
        itemBuilder: (context, index) {
 | 
			
		||||
          final MediaItem item = mediaController.items[index];
 | 
			
		||||
          final bool isActive = index == _currentIndex;
 | 
			
		||||
          final bool isFavorite =
 | 
			
		||||
              item.favorites?.any(
 | 
			
		||||
                (f) => f.userId == authController.userId.value,
 | 
			
		||||
              ) ??
 | 
			
		||||
              false;
 | 
			
		||||
          final bool isReady = _readyItemIds.contains(item.id);
 | 
			
		||||
 | 
			
		||||
          return Scaffold(
 | 
			
		||||
            endDrawer: EndDrawer(),
 | 
			
		||||
            endDrawerEnableOpenDragGesture:
 | 
			
		||||
                mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
            body: CustomScrollView(
 | 
			
		||||
              slivers: [
 | 
			
		||||
                SliverAppBar(
 | 
			
		||||
                  floating: false,
 | 
			
		||||
                  pinned: true,
 | 
			
		||||
                  title: Text('f0ck #${item.id}'),
 | 
			
		||||
                  actions: [
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.fullscreen),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Get.to(
 | 
			
		||||
                          FullScreenMediaView(item: item),
 | 
			
		||||
                          fullscreenDialog: true,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.download),
 | 
			
		||||
                      onPressed: () async {
 | 
			
		||||
                        await _downloadMedia(item);
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    PopupMenuButton<String>(
 | 
			
		||||
                      onSelected: (value) async {
 | 
			
		||||
                        switch (value) {
 | 
			
		||||
                          case 'media':
 | 
			
		||||
                            File file = await DefaultCacheManager()
 | 
			
		||||
                                .getSingleFile(item.mediaUrl);
 | 
			
		||||
                            Uint8List bytes = await file.readAsBytes();
 | 
			
		||||
                            final params = ShareParams(
 | 
			
		||||
                              files: [
 | 
			
		||||
                                XFile.fromData(bytes, mimeType: item.mime),
 | 
			
		||||
                              ],
 | 
			
		||||
                            );
 | 
			
		||||
                            await SharePlus.instance.share(params);
 | 
			
		||||
                            break;
 | 
			
		||||
                          case 'direct_link':
 | 
			
		||||
                            await SharePlus.instance.share(
 | 
			
		||||
                              ShareParams(text: item.mediaUrl),
 | 
			
		||||
                            );
 | 
			
		||||
                            break;
 | 
			
		||||
                          case 'post_link':
 | 
			
		||||
                            await SharePlus.instance.share(
 | 
			
		||||
                              ShareParams(text: item.postUrl),
 | 
			
		||||
                            );
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                      itemBuilder: (context) => [
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'media',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.image),
 | 
			
		||||
                            title: const Text('Als Datei'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'direct_link',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.link),
 | 
			
		||||
                            title: const Text('Link zur Datei'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        PopupMenuItem(
 | 
			
		||||
                          value: 'post_link',
 | 
			
		||||
                          child: ListTile(
 | 
			
		||||
                            leading: const Icon(Icons.article),
 | 
			
		||||
                            title: const Text('Link zum f0ck'),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                      icon: const Icon(Icons.share),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Builder(
 | 
			
		||||
                      builder: (context) => IconButton(
 | 
			
		||||
                        icon: const Icon(Icons.menu),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
            appBar: AppBar(
 | 
			
		||||
              title: Text('f0ck #${item.id}'),
 | 
			
		||||
              actions: [
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.fullscreen),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    Get.to(
 | 
			
		||||
                      FullScreenMediaView(item: item),
 | 
			
		||||
                      fullscreenDialog: true,
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                SliverToBoxAdapter(
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      AnimatedBuilder(
 | 
			
		||||
                        animation: _pageController,
 | 
			
		||||
                        builder: (context, child) {
 | 
			
		||||
                          return buildAnimatedTransition(
 | 
			
		||||
                            context: context,
 | 
			
		||||
                            pageController: _pageController,
 | 
			
		||||
                            index: index,
 | 
			
		||||
                            controller: mediaController,
 | 
			
		||||
                            child: child!,
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                        child: _buildMedia(item, isActive),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const SizedBox(height: 16),
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
			
		||||
                        child: Column(
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                ...item.tags?.map(
 | 
			
		||||
                                  (tag) => Padding(
 | 
			
		||||
                                    padding: const EdgeInsets.only(right: 6),
 | 
			
		||||
                                    child: ActionTag(tag, (onTagTap) {
 | 
			
		||||
                                      if (tag.tag == 'sfw' || tag.tag == 'nsfw') {
 | 
			
		||||
                                        return;
 | 
			
		||||
                                      }
 | 
			
		||||
                                      mediaController.setTag(onTagTap);
 | 
			
		||||
                                      Get.offAllNamed('/');
 | 
			
		||||
                                    }),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ) ?? [],
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            if (authController.isLoggedIn) ...[
 | 
			
		||||
                              const SizedBox(height: 20),
 | 
			
		||||
                              Row(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: SingleChildScrollView(
 | 
			
		||||
                                      scrollDirection: Axis.horizontal,
 | 
			
		||||
                                      child: FavoriteAvatars(
 | 
			
		||||
                                        favorites: item.favorites ?? [],
 | 
			
		||||
                                        brightness: Theme.of(
 | 
			
		||||
                                          context,
 | 
			
		||||
                                        ).brightness,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  IconButton(
 | 
			
		||||
                                    icon: isFavorite
 | 
			
		||||
                                        ? const Icon(Icons.favorite)
 | 
			
		||||
                                        : const Icon(Icons.favorite_outline),
 | 
			
		||||
                                    color: Colors.red,
 | 
			
		||||
                                    onPressed: () async {
 | 
			
		||||
                                      final List<Favorite>? newFavorites =
 | 
			
		||||
                                          await mediaController.toggleFavorite(
 | 
			
		||||
                                            item,
 | 
			
		||||
                                            isFavorite,
 | 
			
		||||
                                          );
 | 
			
		||||
                                      if (newFavorites != null) {
 | 
			
		||||
                                        mediaController.items[index] = item
 | 
			
		||||
                                            .copyWith(favorites: newFavorites);
 | 
			
		||||
                                        mediaController.items.refresh();
 | 
			
		||||
                                      }
 | 
			
		||||
                                      setState(() {});
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.download),
 | 
			
		||||
                  onPressed: () async {
 | 
			
		||||
                    await _downloadMedia(item);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                PopupMenuButton<ShareAction>(
 | 
			
		||||
                  onSelected: (value) => _handleShareAction(value, item),
 | 
			
		||||
                  itemBuilder: (context) => _shareMenuItems,
 | 
			
		||||
                  icon: const Icon(Icons.share),
 | 
			
		||||
                ),
 | 
			
		||||
                Builder(
 | 
			
		||||
                  builder: (context) => IconButton(
 | 
			
		||||
                    icon: const Icon(Icons.menu),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            persistentFooterButtons: [
 | 
			
		||||
              Obx(() {
 | 
			
		||||
                if (mediaController.tag.value != null) {
 | 
			
		||||
                  return Center(
 | 
			
		||||
                    child: InputChip(
 | 
			
		||||
                      label: Text(mediaController.tag.value!),
 | 
			
		||||
                      onDeleted: () {
 | 
			
		||||
                        mediaController.setTag(null);
 | 
			
		||||
                        Get.offAllNamed('/');
 | 
			
		||||
                      },
 | 
			
		||||
            body: SingleChildScrollView(
 | 
			
		||||
              child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                children: [
 | 
			
		||||
                  AnimatedBuilder(
 | 
			
		||||
                    animation: _pageController!,
 | 
			
		||||
                    builder: (context, child) {
 | 
			
		||||
                      return buildAnimatedTransition(
 | 
			
		||||
                        context: context,
 | 
			
		||||
                        pageController: _pageController!,
 | 
			
		||||
                        index: index,
 | 
			
		||||
                        controller: mediaController,
 | 
			
		||||
                        child: child!,
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    child: Obx(
 | 
			
		||||
                      () => _buildMedia(item, index == _currentIndex.value),
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                } else {
 | 
			
		||||
                  return SizedBox.shrink();
 | 
			
		||||
                }
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  if (isReady)
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Wrap(
 | 
			
		||||
                            spacing: 6.0,
 | 
			
		||||
                            runSpacing: 4.0,
 | 
			
		||||
                            alignment: WrapAlignment.center,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              ...item.tags?.map(
 | 
			
		||||
                                    (tag) => ActionTag(
 | 
			
		||||
                                      tag,
 | 
			
		||||
                                      (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
			
		||||
                                          ? (onTagTap) => {}
 | 
			
		||||
                                          : (onTagTap) {
 | 
			
		||||
                                              mediaController.setTag(onTagTap);
 | 
			
		||||
                                              Get.offAllNamed('/');
 | 
			
		||||
                                            },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ) ??
 | 
			
		||||
                                  [],
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                          Obx(
 | 
			
		||||
                            () => Visibility(
 | 
			
		||||
                              visible: authController.isLoggedIn,
 | 
			
		||||
                              child: Padding(
 | 
			
		||||
                                padding: const EdgeInsets.only(top: 20.0),
 | 
			
		||||
                                child: FavoriteSection(
 | 
			
		||||
                                  item: item,
 | 
			
		||||
                                  index: index,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                  else
 | 
			
		||||
                    const SizedBox.shrink(),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            persistentFooterButtons: mediaController.tag.value != null
 | 
			
		||||
                ? [TagFooter()]
 | 
			
		||||
                : null,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:pullex/pullex.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/widgets/tagfooter.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/mediadetail.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/media_tile.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
@@ -24,151 +24,162 @@ class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
    initialRefresh: false,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  late final _MediaGridAppBar _appBar;
 | 
			
		||||
  late final _MediaGridBody _body;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _mediaController.fetchInitial();
 | 
			
		||||
    _appBar = _MediaGridAppBar(mediaController: _mediaController);
 | 
			
		||||
    _body = _MediaGridBody(
 | 
			
		||||
      refreshController: _refreshController,
 | 
			
		||||
      mediaController: _mediaController,
 | 
			
		||||
      scrollController: _scrollController,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _scrollController.dispose();
 | 
			
		||||
    _refreshController.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      endDrawer: EndDrawer(),
 | 
			
		||||
      endDrawerEnableOpenDragGesture: _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
      body: PullexRefresh(
 | 
			
		||||
        controller: _refreshController,
 | 
			
		||||
        enablePullDown: true,
 | 
			
		||||
        enablePullUp: true,
 | 
			
		||||
        header: MaterialHeader(offset: 140),
 | 
			
		||||
        onRefresh: () async {
 | 
			
		||||
          try {
 | 
			
		||||
            if (_mediaController.loading.value) return;
 | 
			
		||||
            if (!_mediaController.atStart.value) {
 | 
			
		||||
              await _mediaController.fetchNewer();
 | 
			
		||||
            } else {
 | 
			
		||||
              await _mediaController.fetchInitial();
 | 
			
		||||
            }
 | 
			
		||||
          } finally {
 | 
			
		||||
            _refreshController.refreshCompleted();
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onLoading: () async {
 | 
			
		||||
          try {
 | 
			
		||||
            if (!_mediaController.loading.value &&
 | 
			
		||||
                !_mediaController.atEnd.value) {
 | 
			
		||||
              await _mediaController.fetchMore();
 | 
			
		||||
            }
 | 
			
		||||
          } finally {
 | 
			
		||||
            _refreshController.loadComplete();
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        child: CustomScrollView(
 | 
			
		||||
          controller: _scrollController,
 | 
			
		||||
          slivers: [
 | 
			
		||||
            SliverAppBar(
 | 
			
		||||
              pinned: false,
 | 
			
		||||
              snap: true,
 | 
			
		||||
              floating: true,
 | 
			
		||||
              title: GestureDetector(
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Image.asset(
 | 
			
		||||
                      'assets/images/f0ck_small.webp',
 | 
			
		||||
                      fit: BoxFit.fitHeight,
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(width: 10),
 | 
			
		||||
                    const Text('fApp', style: TextStyle(fontSize: 24)),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              actions: [
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.search),
 | 
			
		||||
                  onPressed: () async {
 | 
			
		||||
                    await showSearch(
 | 
			
		||||
                      context: context,
 | 
			
		||||
                      delegate: CustomSearchDelegate(),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                Obx(
 | 
			
		||||
                  () => IconButton(
 | 
			
		||||
                    icon: Icon(
 | 
			
		||||
                      _mediaController.random.value == 1
 | 
			
		||||
                          ? Icons.shuffle_on_outlined
 | 
			
		||||
                          : Icons.shuffle,
 | 
			
		||||
                    ),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      _mediaController.toggleRandom();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Builder(
 | 
			
		||||
                  builder: (context) {
 | 
			
		||||
                    return IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.menu),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Scaffold.of(context).openEndDrawer();
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Obx(
 | 
			
		||||
              () => SliverPadding(
 | 
			
		||||
                padding: const EdgeInsets.all(4),
 | 
			
		||||
                sliver: SliverGrid(
 | 
			
		||||
                  delegate: SliverChildBuilderDelegate((context, index) {
 | 
			
		||||
                    final item = _mediaController.filteredItems[index];
 | 
			
		||||
                    return GestureDetector(
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        final item = _mediaController.filteredItems[index];
 | 
			
		||||
                        Get.to(() => MediaDetailScreen(initialId: item.id));
 | 
			
		||||
                      },
 | 
			
		||||
                      child: MediaTile(item: item),
 | 
			
		||||
                    );
 | 
			
		||||
                  }, childCount: _mediaController.filteredItems.length),
 | 
			
		||||
                  gridDelegate: _mediaController.crossAxisCount.value == 0
 | 
			
		||||
                      ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                          maxCrossAxisExtent: 150,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
                          mainAxisSpacing: 5,
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
                        )
 | 
			
		||||
                      : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                          crossAxisCount: _mediaController.crossAxisCount.value,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
                          mainAxisSpacing: 5,
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
                        ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
    return Obx(
 | 
			
		||||
      () => Scaffold(
 | 
			
		||||
        endDrawer: const EndDrawer(),
 | 
			
		||||
        endDrawerEnableOpenDragGesture:
 | 
			
		||||
            _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
        bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
        appBar: _appBar,
 | 
			
		||||
        body: _body,
 | 
			
		||||
        persistentFooterButtons: _mediaController.tag.value != null
 | 
			
		||||
            ? [TagFooter()]
 | 
			
		||||
            : null,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget {
 | 
			
		||||
  const _MediaGridAppBar({required this.mediaController});
 | 
			
		||||
 | 
			
		||||
  final MediaController mediaController;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppBar(
 | 
			
		||||
      title: InkWell(
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          mediaController.setTag(null);
 | 
			
		||||
        },
 | 
			
		||||
        child: Row(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Image.asset('assets/images/f0ck_small.webp', fit: BoxFit.fitHeight),
 | 
			
		||||
            const SizedBox(width: 10),
 | 
			
		||||
            const Text('fApp', style: TextStyle(fontSize: 24)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        IconButton(
 | 
			
		||||
          icon: const Icon(Icons.search),
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            await showSearch(
 | 
			
		||||
              context: context,
 | 
			
		||||
              delegate: CustomSearchDelegate(),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        Obx(
 | 
			
		||||
          () => IconButton(
 | 
			
		||||
            icon: Icon(
 | 
			
		||||
              mediaController.isRandomEnabled
 | 
			
		||||
                  ? Icons.shuffle_on_outlined
 | 
			
		||||
                  : Icons.shuffle,
 | 
			
		||||
            ),
 | 
			
		||||
            onPressed: mediaController.toggleRandom,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          icon: const Icon(Icons.menu),
 | 
			
		||||
          onPressed: () => Scaffold.of(context).openEndDrawer(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaGridBody extends StatelessWidget {
 | 
			
		||||
  const _MediaGridBody({
 | 
			
		||||
    required this.refreshController,
 | 
			
		||||
    required this.mediaController,
 | 
			
		||||
    required this.scrollController,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final PullexRefreshController refreshController;
 | 
			
		||||
  final MediaController mediaController;
 | 
			
		||||
  final ScrollController scrollController;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return PullexRefresh(
 | 
			
		||||
      controller: refreshController,
 | 
			
		||||
      enablePullDown: true,
 | 
			
		||||
      enablePullUp: true,
 | 
			
		||||
      header: const MaterialHeader(),
 | 
			
		||||
      onRefresh: () async {
 | 
			
		||||
        try {
 | 
			
		||||
          await mediaController.handleRefresh();
 | 
			
		||||
        } finally {
 | 
			
		||||
          refreshController.refreshCompleted();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onLoading: () async {
 | 
			
		||||
        try {
 | 
			
		||||
          await mediaController.handleLoading();
 | 
			
		||||
        } finally {
 | 
			
		||||
          refreshController.loadComplete();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      child: Obx(
 | 
			
		||||
        () => GridView.builder(
 | 
			
		||||
          addAutomaticKeepAlives: false,
 | 
			
		||||
          controller: scrollController,
 | 
			
		||||
          physics: const NeverScrollableScrollPhysics(),
 | 
			
		||||
          shrinkWrap: true,
 | 
			
		||||
          padding: const EdgeInsets.all(4),
 | 
			
		||||
          itemCount: mediaController.items.length,
 | 
			
		||||
          gridDelegate: mediaController.crossAxisCount.value == 0
 | 
			
		||||
              ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                  maxCrossAxisExtent: 150,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
                  mainAxisSpacing: 5,
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
                )
 | 
			
		||||
              : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                  crossAxisCount: mediaController.crossAxisCount.value,
 | 
			
		||||
                  crossAxisSpacing: 5,
 | 
			
		||||
                  mainAxisSpacing: 5,
 | 
			
		||||
                  childAspectRatio: 1,
 | 
			
		||||
                ),
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
            final item = mediaController.items[index];
 | 
			
		||||
            return GestureDetector(
 | 
			
		||||
              key: ValueKey(item.id),
 | 
			
		||||
              onTap: () => Get.toNamed('/${item.id}'),
 | 
			
		||||
              child: MediaTile(item: item),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      persistentFooterButtons: [
 | 
			
		||||
        Obx(() {
 | 
			
		||||
          if (_mediaController.tag.value != null) {
 | 
			
		||||
            return Center(
 | 
			
		||||
              child: InputChip(
 | 
			
		||||
                label: Text(_mediaController.tag.value!),
 | 
			
		||||
                onDeleted: () {
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                  Get.offAllNamed('/');
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            return SizedBox.shrink();
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,6 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    onChanged: (int? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setCrossAxisCount(newValue);
 | 
			
		||||
                        setState(() {});
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -93,7 +92,6 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                    onChanged: (PageTransition? newValue) async {
 | 
			
		||||
                      if (newValue != null) {
 | 
			
		||||
                        await controller.setTransitionType(newValue);
 | 
			
		||||
                        setState(() {});
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user