Files
Qlyra/lib/frontend/widgets/floating_video_note.dart
T

157 lines
4.4 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../../core/media/media_playback.dart';
import '../../core/media/video_note_frame.dart';
import '../../core/utils/haptics.dart';
import 'draggable_floating_layer.dart';
class FloatingVideoNoteLayer extends StatelessWidget {
const FloatingVideoNoteLayer({super.key});
@override
Widget build(BuildContext context) {
final playback = MediaPlayback.instance;
return ValueListenableBuilder<VideoNoteTrack?>(
valueListenable: playback.videoNote,
builder: (context, track, _) {
if (track == null) return const SizedBox.shrink();
return ValueListenableBuilder<int?>(
valueListenable: playback.visibleChatId,
builder: (context, chatId, _) => chatId == track.chatId
? const SizedBox.shrink()
: _DraggableNote(track: track),
);
},
);
}
}
class _DraggableNote extends StatelessWidget {
const _DraggableNote({required this.track});
final VideoNoteTrack track;
static const double _size = 96;
void _toggle() {
Haptics.tap();
final controller = track.controller;
if (controller.value.isPlaying) {
controller.pause();
} else {
controller.play();
}
}
@override
Widget build(BuildContext context) {
return DraggableFloatingLayer(
storageKey: 'video_note',
size: const Size(_size, _size),
onTap: _toggle,
child: _NoteCircle(track: track, size: _size),
);
}
}
class _NoteCircle extends StatelessWidget {
const _NoteCircle({required this.track, required this.size});
final VideoNoteTrack track;
final double size;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final frame = videoNoteFrameSize(track.controller.value.size, size);
return SizedBox(
width: size,
height: size,
child: Stack(
children: [
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(3),
child: ClipOval(
child: ColoredBox(
color: cs.surfaceContainerHighest,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.hardEdge,
child: SizedBox(
width: frame.width,
height: frame.height,
child: VideoPlayer(track.controller),
),
),
),
),
),
),
Positioned.fill(
child: IgnorePointer(
child: ValueListenableBuilder<VideoPlayerValue>(
valueListenable: track.controller,
builder: (context, value, _) {
final total = value.duration.inMilliseconds;
return CustomPaint(
painter: _RingPainter(
progress: total > 0
? (value.position.inMilliseconds / total).clamp(
0.0,
1.0,
)
: 0.0,
color: cs.onSurface,
track: cs.onSurface.withValues(alpha: 0.25),
),
);
},
),
),
),
],
),
);
}
}
class _RingPainter extends CustomPainter {
const _RingPainter({
required this.progress,
required this.color,
required this.track,
});
final double progress;
final Color color;
final Color track;
static const double _stroke = 3;
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final circle = rect.deflate(_stroke / 2);
final base = Paint()
..style = PaintingStyle.stroke
..strokeWidth = _stroke
..color = track;
canvas.drawArc(circle, 0, math.pi * 2, false, base);
if (progress <= 0) return;
final arc = Paint()
..style = PaintingStyle.stroke
..strokeWidth = _stroke
..strokeCap = StrokeCap.round
..color = color;
canvas.drawArc(circle, -math.pi / 2, math.pi * 2 * progress, false, arc);
}
@override
bool shouldRepaint(_RingPainter old) =>
old.progress != progress || old.color != color || old.track != track;
}