1.0.17
- new appicon - smartRefreshIndicator (https://github.com/flutter/flutter/issues/65356#issuecomment-2410727567)
| 
		 Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.2 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.6 KiB  | 
| 
		 Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 15 KiB  | 
| 
		 Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 680 KiB After Width: | Height: | Size: 447 KiB  | 
| 
		 Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 18 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 21 KiB  | 
| 
		 Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB  | 
| 
		 Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 1.3 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 38 KiB  | 
| 
		 Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 53 KiB  | 
| 
		 Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 62 KiB  | 
| 
		 Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 68 KiB  | 
| 
		 Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 72 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB  | 
| 
		 Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.8 KiB  | 
| 
		 Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.0 KiB  | 
| 
		 Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.7 KiB  | 
| 
		 Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 192 KiB  | 
| 
		 Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.1 KiB  | 
| 
		 Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.6 KiB  | 
| 
		 Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 6.9 KiB  | 
| 
		 Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 7.3 KiB  | 
| 
		 Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 7.7 KiB  | 
| 
		 Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 8.7 KiB  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.0 KiB  | 
| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB  | 
@@ -15,6 +15,7 @@ class F0ckApp extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return MaterialApp(
 | 
			
		||||
      debugShowCheckedModeBanner: false,
 | 
			
		||||
      theme: ThemeData(
 | 
			
		||||
        scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class MediaGrid extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
  final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  final String _version = '1.0.15';
 | 
			
		||||
  final String _version = '1.0.17';
 | 
			
		||||
  List<MediaItem> mediaItems = [];
 | 
			
		||||
  bool isLoading = false;
 | 
			
		||||
  Timer? _debounceTimer;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:f0ckapp/models/mediaitem.dart';
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
 | 
			
		||||
 | 
			
		||||
class DetailView extends StatefulWidget {
 | 
			
		||||
  final int initialItemId;
 | 
			
		||||
@@ -26,23 +27,23 @@ class DetailView extends StatefulWidget {
 | 
			
		||||
class _DetailViewState extends State<DetailView> {
 | 
			
		||||
  late PageController _pageController;
 | 
			
		||||
  late List<MediaItem> mediaItems;
 | 
			
		||||
  final List<String> _modes = ["sfw", "nsfw", "untagged", "all"];
 | 
			
		||||
  int currentItemId = 0;
 | 
			
		||||
  bool isLoading = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
 | 
			
		||||
    mediaItems = widget.mediaItems;
 | 
			
		||||
    final initialIndex = mediaItems.indexWhere(
 | 
			
		||||
      (item) => item.id == widget.initialItemId,
 | 
			
		||||
    );
 | 
			
		||||
    _pageController = PageController(initialPage: initialIndex);
 | 
			
		||||
 | 
			
		||||
    currentItemId = mediaItems[initialIndex].id;
 | 
			
		||||
 | 
			
		||||
    _pageController.addListener(() {
 | 
			
		||||
    _pageController.addListener(_onPageScroll);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onPageScroll() {
 | 
			
		||||
    final newIndex = _pageController.page?.round();
 | 
			
		||||
    if (newIndex != null && newIndex < mediaItems.length) {
 | 
			
		||||
      setState(() => currentItemId = mediaItems[newIndex].id);
 | 
			
		||||
@@ -52,7 +53,6 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
        _pageController.position.maxScrollExtent - 100) {
 | 
			
		||||
      _loadMoreMedia();
 | 
			
		||||
    }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _loadMoreMedia() async {
 | 
			
		||||
@@ -66,15 +66,11 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
        mode: widget.mode,
 | 
			
		||||
        random: widget.random,
 | 
			
		||||
      );
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
      if (mounted && newMedia.isNotEmpty) {
 | 
			
		||||
        setState(() => mediaItems.addAll(newMedia));
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
          SnackBar(content: Text('Fehler beim Laden weiterer Medien: $e')),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      _showError("Fehler beim Laden weiterer Medien: $e");
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => isLoading = false);
 | 
			
		||||
    }
 | 
			
		||||
@@ -84,22 +80,22 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
    try {
 | 
			
		||||
      final updatedItem = await fetchMediaDetail(currentItemId);
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          final index = mediaItems.indexWhere(
 | 
			
		||||
            (item) => item.id == currentItemId,
 | 
			
		||||
          );
 | 
			
		||||
        final index = mediaItems.indexWhere((item) => item.id == currentItemId);
 | 
			
		||||
        if (index != -1) {
 | 
			
		||||
            mediaItems[index] = updatedItem;
 | 
			
		||||
          setState(() => mediaItems[index] = updatedItem);
 | 
			
		||||
        }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
          SnackBar(content: Text('Fehler beim Aktualisieren des Items: $e')),
 | 
			
		||||
        );
 | 
			
		||||
      _showError("Fehler beim Aktualisieren des Items: $e");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showError(String message) {
 | 
			
		||||
    if (mounted) {
 | 
			
		||||
      ScaffoldMessenger.of(
 | 
			
		||||
        context,
 | 
			
		||||
      ).showSnackBar(SnackBar(content: Text(message)));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -109,70 +105,60 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        backgroundColor: const Color(0xFF2B2B2B),
 | 
			
		||||
        foregroundColor: Colors.white,
 | 
			
		||||
        title: Text(
 | 
			
		||||
          'f0ck #$currentItemId (${widget.type}, ${_modes[widget.mode]})',
 | 
			
		||||
        ),
 | 
			
		||||
        title: Text('f0ck #$currentItemId (${widget.type})'),
 | 
			
		||||
        centerTitle: true,
 | 
			
		||||
      ),
 | 
			
		||||
      body: RefreshIndicator(
 | 
			
		||||
        onRefresh: _refreshMediaItem,
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          child: Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              SizedBox(
 | 
			
		||||
                height: MediaQuery.of(context).size.height,
 | 
			
		||||
                child: PageView.builder(
 | 
			
		||||
      body: PageView.builder(
 | 
			
		||||
        controller: _pageController,
 | 
			
		||||
        itemCount: mediaItems.length,
 | 
			
		||||
        itemBuilder: (context, index) {
 | 
			
		||||
                    final item = mediaItems[index];
 | 
			
		||||
                    return Column(
 | 
			
		||||
          final MediaItem item = mediaItems[index];
 | 
			
		||||
          return Scaffold(
 | 
			
		||||
            body: SafeArea(
 | 
			
		||||
              child: SmartRefreshIndicator(
 | 
			
		||||
                onRefresh: _refreshMediaItem,
 | 
			
		||||
                child: _buildMediaItem(item)
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildMediaItem(MediaItem item) {
 | 
			
		||||
    return SingleChildScrollView(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          if (item.mime.startsWith('image'))
 | 
			
		||||
                          Image.network(item.mediaUrl, fit: BoxFit.contain)
 | 
			
		||||
            Image.network(
 | 
			
		||||
              item.mediaUrl,
 | 
			
		||||
              fit: BoxFit.contain,
 | 
			
		||||
            )
 | 
			
		||||
          else
 | 
			
		||||
            VideoWidget(details: item),
 | 
			
		||||
          const SizedBox(height: 20),
 | 
			
		||||
          Text(
 | 
			
		||||
            item.mime,
 | 
			
		||||
                          style: const TextStyle(
 | 
			
		||||
                            color: Colors.white,
 | 
			
		||||
                            fontSize: 18,
 | 
			
		||||
                          ),
 | 
			
		||||
            style: const TextStyle(color: Colors.white, fontSize: 18),
 | 
			
		||||
          ),
 | 
			
		||||
          const SizedBox(height: 10),
 | 
			
		||||
          Wrap(
 | 
			
		||||
            alignment: WrapAlignment.center,
 | 
			
		||||
            spacing: 5.0,
 | 
			
		||||
            children: item.tags.map((tag) {
 | 
			
		||||
                            Color tagColor;
 | 
			
		||||
                            switch (tag.id) {
 | 
			
		||||
                              case 1:
 | 
			
		||||
                                tagColor = Colors.green;
 | 
			
		||||
                                break;
 | 
			
		||||
                              case 2:
 | 
			
		||||
                                tagColor = Colors.red;
 | 
			
		||||
                                break;
 | 
			
		||||
                              default:
 | 
			
		||||
                                tagColor = const Color(0xFF090909);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
              return Chip(
 | 
			
		||||
                label: Text(tag.tag),
 | 
			
		||||
                              backgroundColor: tagColor,
 | 
			
		||||
                backgroundColor: {
 | 
			
		||||
                  1: Colors.green,
 | 
			
		||||
                  2: Colors.red
 | 
			
		||||
                }[tag.id] ?? const Color(0xFF090909),
 | 
			
		||||
                labelStyle: const TextStyle(color: Colors.white),
 | 
			
		||||
              );
 | 
			
		||||
            }).toList(),
 | 
			
		||||
          ),
 | 
			
		||||
                        const SizedBox(height: 40),
 | 
			
		||||
          const SizedBox(height: 20),
 | 
			
		||||
        ],
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								lib/utils/SmartRefreshIndicator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class SmartRefreshIndicator extends StatelessWidget {
 | 
			
		||||
  final Future<void> Function() onRefresh;
 | 
			
		||||
  final Widget child;
 | 
			
		||||
 | 
			
		||||
  const SmartRefreshIndicator({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.onRefresh,
 | 
			
		||||
    required this.child,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(context) {
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
      builder: (context, constraints) => RefreshIndicator(
 | 
			
		||||
        onRefresh: onRefresh,
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          physics: const AlwaysScrollableScrollPhysics(),
 | 
			
		||||
          child: ConstrainedBox(
 | 
			
		||||
            constraints: BoxConstraints(minHeight: constraints.maxHeight),
 | 
			
		||||
            child: child,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -52,7 +52,9 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
      children: [
 | 
			
		||||
        AspectRatio(
 | 
			
		||||
          aspectRatio: isAudio ? 1.0 : _controller.value.aspectRatio,
 | 
			
		||||
          aspectRatio: _controller.value.isInitialized
 | 
			
		||||
              ? _controller.value.aspectRatio
 | 
			
		||||
              : 9 / 16,
 | 
			
		||||
          child: Stack(
 | 
			
		||||
            alignment: Alignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
@@ -66,8 +68,11 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
                },
 | 
			
		||||
                child: isAudio
 | 
			
		||||
                    ? Image.network(widget.details.coverUrl, fit: BoxFit.cover)
 | 
			
		||||
                    : VideoPlayer(_controller),
 | 
			
		||||
                    : _controller.value.isInitialized
 | 
			
		||||
                    ? VideoPlayer(_controller)
 | 
			
		||||
                    : Center(child: CircularProgressIndicator()),
 | 
			
		||||
              ),
 | 
			
		||||
              if (_controller.value.isInitialized)
 | 
			
		||||
                Align(
 | 
			
		||||
                  alignment: Alignment.bottomCenter,
 | 
			
		||||
                  child: Stack(
 | 
			
		||||
@@ -81,7 +86,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
                          colors: VideoProgressColors(
 | 
			
		||||
                            playedColor: Colors.red,
 | 
			
		||||
                            bufferedColor: Colors.grey,
 | 
			
		||||
                          backgroundColor: Colors.black.withValues(alpha: 0.5),
 | 
			
		||||
                            backgroundColor: Colors.black.withAlpha(128),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
@@ -108,6 +113,7 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        if (_controller.value.isInitialized)
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            child: Row(
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 1.0.15
 | 
			
		||||
version: 1.0.17
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.9.0-172.0.dev
 | 
			
		||||
 
 | 
			
		||||