feat: 实现故事绘本视频播放及修复TTS状态管理
- 添加 video_player 依赖,实现 OSS 视频播放 - 故事有 has_video 时自动切换到绘本 Tab 并初始化播放器 - 修复播放按钮尺寸及 GestureDetector 事件穿透问题 - TTSService 新增 errorTitle 字段,避免跨故事错误状态污染 - 修复 device entity 相关代码 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d29cf88516
commit
d741fd4f5c
@ -29,6 +29,7 @@ abstract class DeviceInfo with _$DeviceInfo {
|
||||
String? macAddress,
|
||||
@Default('') String name,
|
||||
@Default('in_stock') String status,
|
||||
@Default(false) bool isOnline,
|
||||
@Default('') String firmwareVersion,
|
||||
String? lastOnlineAt,
|
||||
String? createdAt,
|
||||
|
||||
@ -296,7 +296,7 @@ as String?,
|
||||
/// @nodoc
|
||||
mixin _$DeviceInfo {
|
||||
|
||||
int get id; String get sn; DeviceType? get deviceType; DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt;
|
||||
int get id; String get sn; DeviceType? get deviceType; DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; bool get isOnline; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt;
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -309,16 +309,16 @@ $DeviceInfoCopyWith<DeviceInfo> get copyWith => _$DeviceInfoCopyWithImpl<DeviceI
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
|
||||
@ -329,7 +329,7 @@ abstract mixin class $DeviceInfoCopyWith<$Res> {
|
||||
factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
});
|
||||
|
||||
|
||||
@ -346,7 +346,7 @@ class _$DeviceInfoCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? isOnline = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
|
||||
@ -355,7 +355,8 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
|
||||
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
|
||||
as bool,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,lastOnlineAt: freezed == lastOnlineAt ? _self.lastOnlineAt : lastOnlineAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
@ -467,10 +468,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@ -488,10 +489,10 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo():
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@ -508,10 +509,10 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@ -523,7 +524,7 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
@JsonSerializable()
|
||||
|
||||
class _DeviceInfo implements DeviceInfo {
|
||||
const _DeviceInfo({required this.id, required this.sn, this.deviceType, this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
|
||||
const _DeviceInfo({required this.id, required this.sn, this.deviceType, this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.isOnline = false, this.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
|
||||
factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json);
|
||||
|
||||
@override final int id;
|
||||
@ -533,6 +534,7 @@ class _DeviceInfo implements DeviceInfo {
|
||||
@override final String? macAddress;
|
||||
@override@JsonKey() final String name;
|
||||
@override@JsonKey() final String status;
|
||||
@override@JsonKey() final bool isOnline;
|
||||
@override@JsonKey() final String firmwareVersion;
|
||||
@override final String? lastOnlineAt;
|
||||
@override final String? createdAt;
|
||||
@ -550,16 +552,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
|
||||
@ -570,7 +572,7 @@ abstract mixin class _$DeviceInfoCopyWith<$Res> implements $DeviceInfoCopyWith<$
|
||||
factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
});
|
||||
|
||||
|
||||
@ -587,7 +589,7 @@ class __$DeviceInfoCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? isOnline = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
return _then(_DeviceInfo(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
|
||||
@ -596,7 +598,8 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
|
||||
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
|
||||
as bool,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,lastOnlineAt: freezed == lastOnlineAt ? _self.lastOnlineAt : lastOnlineAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
|
||||
@ -39,6 +39,7 @@ _DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
|
||||
macAddress: json['mac_address'] as String?,
|
||||
name: json['name'] as String? ?? '',
|
||||
status: json['status'] as String? ?? 'in_stock',
|
||||
isOnline: json['is_online'] as bool? ?? false,
|
||||
firmwareVersion: json['firmware_version'] as String? ?? '',
|
||||
lastOnlineAt: json['last_online_at'] as String?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
@ -53,6 +54,7 @@ Map<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
|
||||
'mac_address': instance.macAddress,
|
||||
'name': instance.name,
|
||||
'status': instance.status,
|
||||
'is_online': instance.isOnline,
|
||||
'firmware_version': instance.firmwareVersion,
|
||||
'last_online_at': instance.lastOnlineAt,
|
||||
'created_at': instance.createdAt,
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show PlatformException;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import '../widgets/gradient_button.dart';
|
||||
import '../widgets/pill_progress_button.dart';
|
||||
@ -34,6 +35,10 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
bool _hasGeneratedVideo = false;
|
||||
bool _isLoadingVideo = false;
|
||||
|
||||
// Video Player
|
||||
VideoPlayerController? _videoController;
|
||||
bool _videoInitialized = false;
|
||||
|
||||
// TTS — uses global TTSService singleton
|
||||
final TTSService _ttsService = TTSService.instance;
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
@ -108,6 +113,15 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
debugPrint('durationStream error (ignored): $e');
|
||||
});
|
||||
|
||||
// Auto-show video tab if story already has a video
|
||||
final hasVideo = _currentStory['has_video'] == true;
|
||||
final videoUrl = _currentStory['video_url'] as String? ?? '';
|
||||
if (hasVideo && videoUrl.isNotEmpty) {
|
||||
_hasGeneratedVideo = true;
|
||||
_activeTab = 'video';
|
||||
_initVideoPlayer(videoUrl);
|
||||
}
|
||||
|
||||
// Check if audio already exists
|
||||
debugPrint('[StoryDetail] story keys: ${_currentStory.keys.toList()}');
|
||||
debugPrint('[StoryDetail] audio_url value: "${_currentStory['audio_url']}"');
|
||||
@ -151,12 +165,33 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _initVideoPlayer(String url) async {
|
||||
try {
|
||||
final controller = VideoPlayerController.networkUrl(Uri.parse(url));
|
||||
_videoController = controller;
|
||||
controller.addListener(_onVideoChanged);
|
||||
await controller.initialize();
|
||||
if (mounted) {
|
||||
setState(() => _videoInitialized = true);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Video init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onVideoChanged() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ttsService.removeListener(_onTTSChanged);
|
||||
_positionSub?.cancel();
|
||||
_playerStateSub?.cancel();
|
||||
_audioPlayer.dispose();
|
||||
_videoController?.removeListener(_onVideoChanged);
|
||||
_videoController?.dispose();
|
||||
_genieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -171,7 +206,8 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
|
||||
if (_ttsService.error != null &&
|
||||
!_ttsService.isGenerating &&
|
||||
_ttsService.audioUrl == null) {
|
||||
_ttsService.audioUrl == null &&
|
||||
_ttsService.errorTitle == title) {
|
||||
return TTSButtonState.error;
|
||||
}
|
||||
if (_ttsService.isGeneratingFor(title)) {
|
||||
@ -518,28 +554,58 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: const Center(
|
||||
child: Icon(Icons.videocam, color: Colors.white54, size: 48),
|
||||
// Not yet initialized — black + spinner while video loads
|
||||
if (!_videoInitialized || _videoController == null) {
|
||||
return const AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: Colors.white54, strokeWidth: 3),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Initialized: VideoPlayer shows frame 0 naturally as thumbnail when paused
|
||||
final isPlaying = _videoController!.value.isPlaying;
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _videoController!.value.aspectRatio,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Video fills the area (Positioned.fill avoids StackFit.expand distortion)
|
||||
Positioned.fill(child: VideoPlayer(_videoController!)),
|
||||
// Full-area tap handler
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
if (_videoController!.value.isPlaying) {
|
||||
await _videoController!.pause();
|
||||
} else {
|
||||
await _videoController!.play();
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.play_arrow, color: Colors.black),
|
||||
),
|
||||
],
|
||||
// Play button — IgnorePointer lets taps pass through to GestureDetector below
|
||||
if (!isPlaying)
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.play_arrow, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ class TTSService extends ChangeNotifier {
|
||||
|
||||
// ── Error ──
|
||||
String? _error;
|
||||
String? _errorTitle; // Which story the error belongs to
|
||||
|
||||
// ── Getters ──
|
||||
bool get isGenerating => _isGenerating;
|
||||
@ -39,6 +40,7 @@ class TTSService extends ChangeNotifier {
|
||||
String? get completedStoryTitle => _completedStoryTitle;
|
||||
bool get justCompleted => _justCompleted;
|
||||
String? get error => _error;
|
||||
String? get errorTitle => _errorTitle;
|
||||
|
||||
/// Check if audio is ready for a specific story.
|
||||
bool hasAudioFor(String title) {
|
||||
@ -182,6 +184,7 @@ class TTSService extends ChangeNotifier {
|
||||
_isGenerating = false;
|
||||
if (_audioUrl == null) {
|
||||
_error = '未获取到音频';
|
||||
_errorTitle = title;
|
||||
_statusMessage = '生成失败';
|
||||
}
|
||||
notifyListeners();
|
||||
@ -190,6 +193,7 @@ class TTSService extends ChangeNotifier {
|
||||
_isGenerating = false;
|
||||
_progress = 0.0;
|
||||
_error = e.toString();
|
||||
_errorTitle = title;
|
||||
_statusMessage = '生成失败';
|
||||
_justCompleted = false;
|
||||
notifyListeners();
|
||||
@ -212,6 +216,7 @@ class TTSService extends ChangeNotifier {
|
||||
_completedStoryTitle = null;
|
||||
_justCompleted = false;
|
||||
_error = null;
|
||||
_errorTitle = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import file_selector_macos
|
||||
import flutter_blue_plus_darwin
|
||||
import just_audio
|
||||
import shared_preferences_foundation
|
||||
import video_player_avfoundation
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
||||
@ -232,6 +232,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
custom_lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -540,6 +548,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1265,6 +1281,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "08bfba72e311d48219acad4e191b1f9c27ff8cf928f2c7234874592d9c9d7341"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "9862c67c4661c98f30fe707bc1a4f97d6a0faa76784f485d282668e4651a7ac3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.3"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -67,6 +67,7 @@ dependencies:
|
||||
image_picker: ^1.2.1
|
||||
just_audio: ^0.9.42
|
||||
http: ^1.2.0
|
||||
video_player: ^2.9.2
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user