v1.4.4+65
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s

This commit is contained in:
2025-06-22 03:02:18 +02:00
parent 7f0743808a
commit 95f6dcfe2b
13 changed files with 461 additions and 339 deletions

View File

@ -16,7 +16,7 @@ class MediaTile extends StatelessWidget {
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: 'https://f0ck.me/t/${item.id}.webp',
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Container(color: Colors.grey[900]),
errorWidget: (context, url, error) =>

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/models/item.dart';
import 'package:f0ckapp/widgets/actiontag.dart';
import 'package:f0ckapp/controller/mediacontroller.dart';
class TagSection extends StatefulWidget {
final List<Tag> tags;
const TagSection({super.key, required this.tags});
@override
State<TagSection> createState() => _TagSectionState();
}
class _TagSectionState extends State<TagSection> {
bool _areTagsExpanded = false;
@override
Widget build(BuildContext context) {
final MediaController mediaController = Get.find<MediaController>();
final bool hasMoreTags = widget.tags.length > 5;
final List<Tag> tagsToShow = _areTagsExpanded
? widget.tags
: widget.tags.take(5).toList();
return Column(
children: [
Wrap(
spacing: 6.0,
runSpacing: 4.0,
alignment: WrapAlignment.center,
children: [
...tagsToShow.map(
(tag) => ActionTag(
tag,
(tag.tag == 'sfw' || tag.tag == 'nsfw')
? (onTagTap) => {}
: (onTagTap) {
mediaController.setTag(onTagTap);
Get.offAllNamed('/');
},
),
),
],
),
if (hasMoreTags)
TextButton(
onPressed: () {
setState(() => _areTagsExpanded = !_areTagsExpanded);
},
child: Text(
_areTagsExpanded
? 'Weniger anzeigen'
: 'Alle ${widget.tags.length} Tags anzeigen',
),
),
],
);
}
}

View File

@ -94,81 +94,99 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
),
),
),
_ControlButton(
widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
() {
IconButton(
icon: Icon(
widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
size: 64,
),
onPressed: () {
widget.onOverlayTap();
widget.controller.value.isPlaying
? widget.controller.pause()
: widget.controller.play();
},
size: 64,
),
Positioned(
right: 12,
bottom: 12,
child: _ControlButton(
widget.muted ? Icons.volume_off : Icons.volume_up,
() {
child: IconButton(
icon: Icon(
widget.muted ? Icons.volume_off : Icons.volume_up,
size: 16,
),
onPressed: () {
widget.onOverlayTap();
widget.onMuteToggle();
},
size: 16,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 10,
bottom: 12,
child: Text(
'${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
Listener(
onPointerDown: (_) {
widget.onOverlayTap();
},
child: VideoProgressIndicator(
widget.controller,
allowScrubbing: true,
padding: const EdgeInsets.only(top: 25.0),
colors: const VideoProgressColors(
playedColor: Colors.red,
backgroundColor: Colors.grey,
bufferedColor: Colors.white54,
),
),
),
if (widget.controller.value.duration.inMilliseconds > 0)
Positioned(
left:
(widget.controller.value.position.inMilliseconds /
widget
.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),
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 10,
bottom: 12,
child: Text(
'${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
],
Listener(
onPointerDown: (_) {
widget.onOverlayTap();
},
child: VideoProgressIndicator(
widget.controller,
allowScrubbing: true,
padding: const EdgeInsets.only(top: 25.0),
colors: VideoProgressColors(
playedColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(
context,
).colorScheme.surface.withValues(alpha: 0.5),
bufferedColor: Theme.of(
context,
).colorScheme.secondary.withValues(alpha: 0.5),
),
),
),
if (widget.controller.value.duration.inMilliseconds > 0)
Positioned(
left:
(widget.controller.value.position.inMilliseconds /
widget
.controller
.value
.duration
.inMilliseconds) *
constraints.maxWidth -
6,
bottom: -4,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
),
),
],
);
},
),
),
),
@ -182,25 +200,3 @@ class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
}
}
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, size: size),
onPressed: onPressed,
),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:get/get.dart';
import 'package:f0ckapp/models/item.dart';
import 'package:f0ckapp/widgets/video_controls_overlay.dart';
import 'package:f0ckapp/controller/settingscontroller.dart';
import 'package:f0ckapp/controller/mediacontroller.dart';
class VideoWidget extends StatefulWidget {
@ -29,8 +30,9 @@ class VideoWidget extends StatefulWidget {
}
class _VideoWidgetState extends State<VideoWidget> {
final MediaController controller = Get.find<MediaController>();
late CachedVideoPlayerPlusController _controller;
final MediaController mediaController = Get.find<MediaController>();
final SettingsController settingsController = Get.find<SettingsController>();
late CachedVideoPlayerPlusController videoController;
late Worker _muteWorker;
late Worker _timerResetWorker;
late Worker _hideControlsWorker;
@ -41,12 +43,14 @@ class _VideoWidgetState extends State<VideoWidget> {
void initState() {
super.initState();
_initController();
_muteWorker = ever(controller.muted, (bool muted) {
if (_controller.value.isInitialized) {
_controller.setVolume(muted ? 0.0 : 1.0);
_muteWorker = ever(settingsController.muted, (bool muted) {
if (videoController.value.isInitialized) {
videoController.setVolume(muted ? 0.0 : 1.0);
}
});
_timerResetWorker = ever(controller.videoControlsTimerNotifier, (_) {
_timerResetWorker = ever(settingsController.videoControlsTimerNotifier, (
_,
) {
if (widget.isActive && mounted) {
if (!_showControls) {
setState(() => _showControls = true);
@ -54,7 +58,7 @@ class _VideoWidgetState extends State<VideoWidget> {
_startHideControlsTimer();
}
});
_hideControlsWorker = ever(controller.hideControlsNotifier, (_) {
_hideControlsWorker = ever(settingsController.hideControlsNotifier, (_) {
if (mounted && _showControls) {
setState(() => _showControls = false);
_hideControlsTimer?.cancel();
@ -63,20 +67,18 @@ class _VideoWidgetState extends State<VideoWidget> {
}
Future<void> _initController() async {
_controller = CachedVideoPlayerPlusController.networkUrl(
videoController = CachedVideoPlayerPlusController.networkUrl(
Uri.parse(widget.details.mediaUrl),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
await _controller.initialize();
await videoController.initialize();
widget.onInitialized?.call();
if (!mounted) return;
setState(() {});
_controller.addListener(() => setState(() {}));
_controller.setLooping(true);
_controller.setVolume(controller.muted.value ? 0.0 : 1.0);
videoController.setLooping(true);
videoController.setVolume(settingsController.muted.value ? 0.0 : 1.0);
if (widget.isActive) {
_controller.play();
videoController.play();
}
}
@ -85,9 +87,9 @@ class _VideoWidgetState extends State<VideoWidget> {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
if (widget.isActive) {
_controller.play();
videoController.play();
} else {
_controller.pause();
videoController.pause();
}
}
}
@ -97,7 +99,7 @@ class _VideoWidgetState extends State<VideoWidget> {
_muteWorker.dispose();
_timerResetWorker.dispose();
_hideControlsWorker.dispose();
_controller.dispose();
videoController.dispose();
_hideControlsTimer?.cancel();
super.dispose();
}
@ -129,7 +131,7 @@ class _VideoWidgetState extends State<VideoWidget> {
@override
Widget build(BuildContext context) {
final bool muted = controller.muted.value;
final bool muted = settingsController.muted.value;
bool isAudio = widget.details.mime.startsWith('audio');
Widget mediaContent;
@ -144,36 +146,43 @@ class _VideoWidgetState extends State<VideoWidget> {
),
);
} else {
mediaContent = _controller.value.isInitialized
? CachedVideoPlayerPlus(_controller)
mediaContent = videoController.value.isInitialized
? CachedVideoPlayerPlus(videoController)
: const Center(child: CircularProgressIndicator());
}
return AspectRatio(
aspectRatio: _controller.value.isInitialized
? _controller.value.aspectRatio
aspectRatio: videoController.value.isInitialized
? videoController.value.aspectRatio
: (isAudio ? 16 / 9 : 9 / 16),
child: Stack(
alignment: Alignment.center,
children: [
GestureDetector(onTap: _onTap, child: mediaContent),
if (_controller.value.isInitialized && _showControls)
Positioned.fill(
child: GestureDetector(
onTap: _onTap,
child: Container(
color: Colors.black.withValues(alpha: 0.5),
child: VideoControlsOverlay(
controller: _controller,
onOverlayTap: () => _onTap(ctrlButton: true),
muted: muted,
onMuteToggle: () {
controller.toggleMuted();
},
AnimatedBuilder(
animation: videoController,
builder: (context, child) {
if (videoController.value.isInitialized && _showControls) {
return Positioned.fill(
child: GestureDetector(
onTap: _onTap,
child: Container(
color: Colors.black.withValues(alpha: 0.5),
child: VideoControlsOverlay(
controller: videoController,
onOverlayTap: () => _onTap(ctrlButton: true),
muted: muted,
onMuteToggle: () {
settingsController.toggleMuted();
},
),
),
),
),
),
),
);
}
return const SizedBox.shrink();
},
),
],
),
);