chore: prepare yanting monorepo handoff

This commit is contained in:
2026-06-03 10:39:03 +09:00
commit 634ae98dec
65 changed files with 5144 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import '../data/models/models.dart';
import '../theme/wise_tokens.dart';
import 'app_card.dart';
class PlayerStateModel {
const PlayerStateModel({
this.audioId = '',
this.reportId = '',
this.title = '',
this.durationSec = 0,
this.positionSec = 0,
this.playing = false,
this.speed = 1.0,
});
final String audioId;
final String reportId;
final String title;
final int durationSec;
final int positionSec;
final bool playing;
final double speed;
bool get hasAudio => audioId.isNotEmpty;
PlayerStateModel copyWith({
String? audioId,
String? reportId,
String? title,
int? durationSec,
int? positionSec,
bool? playing,
double? speed,
}) {
return PlayerStateModel(
audioId: audioId ?? this.audioId,
reportId: reportId ?? this.reportId,
title: title ?? this.title,
durationSec: durationSec ?? this.durationSec,
positionSec: positionSec ?? this.positionSec,
playing: playing ?? this.playing,
speed: speed ?? this.speed,
);
}
}
class MiniPlayer extends StatelessWidget {
const MiniPlayer({
required this.player,
required this.onToggle,
super.key,
});
final PlayerStateModel player;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
if (!player.hasAudio) return const SizedBox.shrink();
final ratio = player.durationSec == 0 ? 0.0 : player.positionSec / player.durationSec;
return Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: AppCard(
padding: const EdgeInsets.all(12),
color: WiseColors.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton.filled(
onPressed: onToggle,
icon: Icon(player.playing ? Icons.pause : Icons.play_arrow),
style: IconButton.styleFrom(
backgroundColor: WiseColors.secondary,
foregroundColor: WiseColors.primary,
),
),
const SizedBox(width: WiseSpacing.x2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
player.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800),
),
Text(
'${formatDuration(player.positionSec)} / ${formatDuration(player.durationSec)} · ${player.speed.toStringAsFixed(1)}x',
style: const TextStyle(color: Color(0xCCFFFFFF), fontSize: 12),
),
],
),
),
],
),
const SizedBox(height: WiseSpacing.x2),
LinearProgressIndicator(
value: ratio.clamp(0, 1),
minHeight: 4,
backgroundColor: const Color(0x33FFFFFF),
color: WiseColors.secondary,
),
],
),
),
);
}
}
class PlayerCard extends StatelessWidget {
const PlayerCard({
required this.title,
required this.durationSec,
required this.player,
required this.onStart,
required this.onToggle,
required this.onSeek,
required this.onSpeed,
super.key,
});
final String title;
final int durationSec;
final PlayerStateModel player;
final VoidCallback onStart;
final VoidCallback onToggle;
final void Function(int delta) onSeek;
final VoidCallback onSpeed;
@override
Widget build(BuildContext context) {
final active = player.hasAudio && player.title == title;
final position = active ? player.positionSec : 0;
final ratio = durationSec == 0 ? 0.0 : position / durationSec;
return AppCard(
color: WiseColors.secondary200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('音频解读', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: WiseSpacing.x2),
Text(title, style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: WiseSpacing.x3),
LinearProgressIndicator(
value: ratio.clamp(0, 1),
minHeight: 6,
backgroundColor: Colors.white,
color: WiseColors.accent,
),
const SizedBox(height: WiseSpacing.x2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(formatDuration(position), style: Theme.of(context).textTheme.bodySmall),
Text(formatDuration(durationSec), style: Theme.of(context).textTheme.bodySmall),
],
),
const SizedBox(height: WiseSpacing.x3),
Row(
children: [
IconButton.outlined(onPressed: () => onSeek(-15), icon: const Icon(Icons.replay_10)),
IconButton.filled(
onPressed: active ? onToggle : onStart,
icon: Icon(active && player.playing ? Icons.pause : Icons.play_arrow),
style: IconButton.styleFrom(
backgroundColor: WiseColors.primary,
foregroundColor: Colors.white,
),
),
IconButton.outlined(onPressed: () => onSeek(15), icon: const Icon(Icons.forward_10)),
const Spacer(),
TextButton(onPressed: onSpeed, child: Text('${player.speed.toStringAsFixed(1)}x')),
],
),
Text(
'真实音频流待 /audio/{id}/stream 接入,当前为本地进度占位。',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}