This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/models/mediaitem.dart';
 | 
			
		||||
@@ -13,7 +14,7 @@ class MediaGrid extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
  final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  final String _version = '1.0.20+20';
 | 
			
		||||
  final String _version = '1.0.21+21';
 | 
			
		||||
  List<MediaItem> mediaItems = [];
 | 
			
		||||
  bool isLoading = false;
 | 
			
		||||
  Timer? _debounceTimer;
 | 
			
		||||
@@ -243,18 +244,28 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
            }
 | 
			
		||||
            final item = mediaItems[index];
 | 
			
		||||
 | 
			
		||||
            final mode =
 | 
			
		||||
                {1: Colors.green, 2: Colors.red}[item.mode] ?? Colors.yellow;
 | 
			
		||||
 | 
			
		||||
            return InkWell(
 | 
			
		||||
              onTap: () => _navigateToDetail(item),
 | 
			
		||||
              child: Stack(
 | 
			
		||||
                fit: StackFit.expand,
 | 
			
		||||
                children: <Widget>[
 | 
			
		||||
                  Image.network(item.thumbnailUrl, fit: BoxFit.cover),
 | 
			
		||||
                  CachedNetworkImage(
 | 
			
		||||
                    imageUrl: item.thumbnailUrl,
 | 
			
		||||
                    fit: BoxFit.cover,
 | 
			
		||||
                    placeholder: (context, url) => CircularProgressIndicator(),
 | 
			
		||||
                    errorWidget: (context, url, error) => Icon(Icons.error),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Align(
 | 
			
		||||
                    alignment: FractionalOffset.bottomRight,
 | 
			
		||||
                    child: Icon(Icons.square, color: mode, size: 15.0),
 | 
			
		||||
                    child: Icon(
 | 
			
		||||
                      Icons.square,
 | 
			
		||||
                      color: switch (item.mode) {
 | 
			
		||||
                        1 => Colors.green,
 | 
			
		||||
                        2 => Colors.red,
 | 
			
		||||
                        _ => Colors.yellow
 | 
			
		||||
                      },
 | 
			
		||||
                      size: 15.0
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:f0ckapp/models/mediaitem.dart';
 | 
			
		||||
import 'package:f0ckapp/services/api.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
@@ -131,9 +132,11 @@ class _DetailViewState extends State<DetailView> {
 | 
			
		||||
      child: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          if (item.mime.startsWith('image'))
 | 
			
		||||
            Image.network(
 | 
			
		||||
              item.mediaUrl,
 | 
			
		||||
            CachedNetworkImage(
 | 
			
		||||
              imageUrl: item.mediaUrl,
 | 
			
		||||
              fit: BoxFit.contain,
 | 
			
		||||
              placeholder: (context, url) => CircularProgressIndicator(),
 | 
			
		||||
              errorWidget: (context, url, error) => Icon(Icons.error),
 | 
			
		||||
            )
 | 
			
		||||
          else
 | 
			
		||||
            VideoWidget(details: item),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								lib/widgets/video_overlay.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								lib/widgets/video_overlay.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:video_player/video_player.dart';
 | 
			
		||||
 | 
			
		||||
class VideoControlsOverlay extends StatelessWidget {
 | 
			
		||||
  final VideoPlayerController controller;
 | 
			
		||||
  const VideoControlsOverlay({super.key, required this.controller});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Stack(
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
        Center(
 | 
			
		||||
          child: Row(
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              _ControlButton(Icons.replay_10, () {
 | 
			
		||||
                controller.seekTo(
 | 
			
		||||
                  controller.value.position - Duration(seconds: 10),
 | 
			
		||||
                );
 | 
			
		||||
              }),
 | 
			
		||||
              SizedBox(width: 32),
 | 
			
		||||
              _ControlButton(
 | 
			
		||||
                controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
			
		||||
                () {
 | 
			
		||||
                  controller.value.isPlaying
 | 
			
		||||
                      ? controller.pause()
 | 
			
		||||
                      : controller.play();
 | 
			
		||||
                },
 | 
			
		||||
                size: 64,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(width: 32),
 | 
			
		||||
              _ControlButton(Icons.forward_10, () {
 | 
			
		||||
                controller.seekTo(
 | 
			
		||||
                  controller.value.position + Duration(seconds: 10),
 | 
			
		||||
                );
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        _ProgressIndicator(controller: controller),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ControlButton extends StatelessWidget {
 | 
			
		||||
  final IconData icon;
 | 
			
		||||
  final VoidCallback onPressed;
 | 
			
		||||
  final double size;
 | 
			
		||||
 | 
			
		||||
  const _ControlButton(this.icon, this.onPressed, {this.size = 24});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      decoration: BoxDecoration(
 | 
			
		||||
        shape: BoxShape.circle,
 | 
			
		||||
        color: Colors.black.withValues(alpha: 0.4),
 | 
			
		||||
      ),
 | 
			
		||||
      child: IconButton(
 | 
			
		||||
        icon: Icon(icon, color: Colors.white, size: size),
 | 
			
		||||
        onPressed: onPressed,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ProgressIndicator extends StatelessWidget {
 | 
			
		||||
  final VideoPlayerController controller;
 | 
			
		||||
  const _ProgressIndicator({required this.controller});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Align(
 | 
			
		||||
      alignment: Alignment.bottomCenter,
 | 
			
		||||
      child: Stack(
 | 
			
		||||
        clipBehavior: Clip.none,
 | 
			
		||||
        children: [
 | 
			
		||||
          Container(
 | 
			
		||||
            height: 20,
 | 
			
		||||
            alignment: Alignment.bottomCenter,
 | 
			
		||||
            color: Colors.transparent,
 | 
			
		||||
            child: VideoProgressIndicator(
 | 
			
		||||
              controller,
 | 
			
		||||
              allowScrubbing: true,
 | 
			
		||||
              colors: VideoProgressColors(
 | 
			
		||||
                playedColor: Colors.red,
 | 
			
		||||
                backgroundColor: Colors.grey,
 | 
			
		||||
                bufferedColor: Colors.white54,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          Positioned(
 | 
			
		||||
            left:
 | 
			
		||||
                (controller.value.position.inMilliseconds /
 | 
			
		||||
                        controller.value.duration.inMilliseconds) *
 | 
			
		||||
                    MediaQuery.of(context).size.width -
 | 
			
		||||
                6,
 | 
			
		||||
            bottom: -4,
 | 
			
		||||
            child: Container(
 | 
			
		||||
              width: 12,
 | 
			
		||||
              height: 12,
 | 
			
		||||
              decoration: BoxDecoration(
 | 
			
		||||
                shape: BoxShape.circle,
 | 
			
		||||
                color: Colors.red,
 | 
			
		||||
                border: Border.all(color: Colors.red, width: 2),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          Positioned(
 | 
			
		||||
            left: 16,
 | 
			
		||||
            bottom: 10,
 | 
			
		||||
            child: Text(
 | 
			
		||||
              '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
 | 
			
		||||
              style: TextStyle(color: Colors.white),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatDuration(Duration duration) {
 | 
			
		||||
    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
			
		||||
    final minutes = twoDigits(duration.inMinutes.remainder(60));
 | 
			
		||||
    final seconds = twoDigits(duration.inSeconds.remainder(60));
 | 
			
		||||
    return "$minutes:$seconds";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import 'package:f0ckapp/models/mediaitem.dart';
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:video_player/video_player.dart';
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:f0ckapp/models/mediaitem.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_overlay.dart';
 | 
			
		||||
 | 
			
		||||
class VideoWidget extends StatefulWidget {
 | 
			
		||||
  final MediaItem details;
 | 
			
		||||
@@ -12,6 +16,7 @@ class VideoWidget extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
  late VideoPlayerController _controller;
 | 
			
		||||
  bool _showControls = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
@@ -37,13 +42,6 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatDuration(Duration duration) {
 | 
			
		||||
    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
			
		||||
    final minutes = twoDigits(duration.inMinutes.remainder(60));
 | 
			
		||||
    final seconds = twoDigits(duration.inSeconds.remainder(60));
 | 
			
		||||
    return "$minutes:$seconds";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
			
		||||
@@ -61,115 +59,38 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
              GestureDetector(
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  setState(() {
 | 
			
		||||
                    _controller.value.isPlaying
 | 
			
		||||
                        ? _controller.pause()
 | 
			
		||||
                        : _controller.play();
 | 
			
		||||
                    _showControls = !_showControls;
 | 
			
		||||
                  });
 | 
			
		||||
                },
 | 
			
		||||
                child: isAudio
 | 
			
		||||
                    ? Image.network(widget.details.coverUrl, fit: BoxFit.cover)
 | 
			
		||||
                    ? CachedNetworkImage(
 | 
			
		||||
                        imageUrl: widget.details.coverUrl,
 | 
			
		||||
                        fit: BoxFit.cover,
 | 
			
		||||
                        placeholder: (context, url) =>
 | 
			
		||||
                            CircularProgressIndicator(),
 | 
			
		||||
                        errorWidget: (context, url, error) => Image.network(
 | 
			
		||||
                          "https://f0ck.me/s/img/music.webp",
 | 
			
		||||
                          fit: BoxFit.contain,
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                    : _controller.value.isInitialized
 | 
			
		||||
                    ? VideoPlayer(_controller)
 | 
			
		||||
                    : Center(child: CircularProgressIndicator()),
 | 
			
		||||
              ),
 | 
			
		||||
              if (_controller.value.isInitialized)
 | 
			
		||||
                Align(
 | 
			
		||||
                  alignment: Alignment.bottomCenter,
 | 
			
		||||
                  child: Stack(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      SizedBox(
 | 
			
		||||
                        height: 10,
 | 
			
		||||
                        child: VideoProgressIndicator(
 | 
			
		||||
                          _controller,
 | 
			
		||||
                          allowScrubbing: true,
 | 
			
		||||
                          padding: EdgeInsets.only(bottom: 0),
 | 
			
		||||
                          colors: VideoProgressColors(
 | 
			
		||||
                            playedColor: Colors.red,
 | 
			
		||||
                            bufferedColor: Colors.grey,
 | 
			
		||||
                            backgroundColor: Colors.black.withAlpha(128),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      Positioned(
 | 
			
		||||
                        left:
 | 
			
		||||
                            (_controller.value.position.inMilliseconds /
 | 
			
		||||
                                    _controller.value.duration.inMilliseconds) *
 | 
			
		||||
                                MediaQuery.of(context).size.width -
 | 
			
		||||
                            6,
 | 
			
		||||
                        bottom: -1,
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          width: 12,
 | 
			
		||||
                          height: 12,
 | 
			
		||||
                          decoration: BoxDecoration(
 | 
			
		||||
                            shape: BoxShape.circle,
 | 
			
		||||
                            color: Colors.white,
 | 
			
		||||
                            border: Border.all(color: Colors.red, width: 2),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
              if (_controller.value.isInitialized && _showControls) ...[
 | 
			
		||||
                IgnorePointer(
 | 
			
		||||
                  ignoring: true,
 | 
			
		||||
                  child: Container(
 | 
			
		||||
                    color: Colors.black.withValues(alpha: 0.5),
 | 
			
		||||
                    width: double.infinity,
 | 
			
		||||
                    height: double.infinity,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                VideoControlsOverlay(controller: _controller),
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        if (_controller.value.isInitialized)
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            child: Row(
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(
 | 
			
		||||
                  _formatDuration(_controller.value.position),
 | 
			
		||||
                  style: const TextStyle(color: Colors.white),
 | 
			
		||||
                ),
 | 
			
		||||
                Row(
 | 
			
		||||
                  mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Icons.replay_10),
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        _controller.seekTo(
 | 
			
		||||
                          _controller.value.position -
 | 
			
		||||
                              const Duration(seconds: 10),
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                      icon: Icon(
 | 
			
		||||
                        _controller.value.isPlaying
 | 
			
		||||
                            ? Icons.pause
 | 
			
		||||
                            : Icons.play_arrow,
 | 
			
		||||
                      ),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        setState(() {
 | 
			
		||||
                          _controller.value.isPlaying
 | 
			
		||||
                              ? _controller.pause()
 | 
			
		||||
                              : _controller.play();
 | 
			
		||||
                        });
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                      icon: const Icon(Icons.forward_10),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        _controller.seekTo(
 | 
			
		||||
                          _controller.value.position +
 | 
			
		||||
                              const Duration(seconds: 10),
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                Text(
 | 
			
		||||
                  _formatDuration(_controller.value.duration),
 | 
			
		||||
                  style: const TextStyle(color: Colors.white),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user