Compare commits

..

No commits in common. "main" and "fix/auto-20260228-145348" have entirely different histories.

11 changed files with 45 additions and 195 deletions

View File

@ -29,7 +29,6 @@ abstract class DeviceInfo with _$DeviceInfo {
String? macAddress, String? macAddress,
@Default('') String name, @Default('') String name,
@Default('in_stock') String status, @Default('in_stock') String status,
@Default(false) bool isOnline,
@Default('') String firmwareVersion, @Default('') String firmwareVersion,
String? lastOnlineAt, String? lastOnlineAt,
String? createdAt, String? createdAt,

View File

@ -296,7 +296,7 @@ as String?,
/// @nodoc /// @nodoc
mixin _$DeviceInfo { mixin _$DeviceInfo {
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; 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;
/// Create a copy of DeviceInfo /// Create a copy of DeviceInfo
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -309,16 +309,16 @@ $DeviceInfoCopyWith<DeviceInfo> get copyWith => _$DeviceInfoCopyWithImpl<DeviceI
@override @override
bool operator ==(Object other) { 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.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)); 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));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt); int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
@override @override
String toString() { String toString() {
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)'; return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
} }
@ -329,7 +329,7 @@ abstract mixin class $DeviceInfoCopyWith<$Res> {
factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl; factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
}); });
@ -346,7 +346,7 @@ class _$DeviceInfoCopyWithImpl<$Res>
/// Create a copy of DeviceInfo /// Create a copy of DeviceInfo
/// with the given fields replaced by the non-null parameter values. /// 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? isOnline = 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? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
@ -355,8 +355,7 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable 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?,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,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // 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,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?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as String?, as String?,
@ -468,10 +467,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, bool isOnline, 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, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo() when $default != null: case _DeviceInfo() when $default != null:
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 $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
return orElse(); return orElse();
} }
@ -489,10 +488,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, bool isOnline, 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, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo(): case _DeviceInfo():
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 $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@ -509,10 +508,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, bool isOnline, 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, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo() when $default != null: case _DeviceInfo() when $default != null:
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 $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
return null; return null;
} }
@ -524,7 +523,7 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
@JsonSerializable() @JsonSerializable()
class _DeviceInfo implements DeviceInfo { 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.isOnline = false, 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.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json); factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json);
@override final int id; @override final int id;
@ -534,7 +533,6 @@ class _DeviceInfo implements DeviceInfo {
@override final String? macAddress; @override final String? macAddress;
@override@JsonKey() final String name; @override@JsonKey() final String name;
@override@JsonKey() final String status; @override@JsonKey() final String status;
@override@JsonKey() final bool isOnline;
@override@JsonKey() final String firmwareVersion; @override@JsonKey() final String firmwareVersion;
@override final String? lastOnlineAt; @override final String? lastOnlineAt;
@override final String? createdAt; @override final String? createdAt;
@ -552,16 +550,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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.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)); 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));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt); int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
@override @override
String toString() { String toString() {
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)'; return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
} }
@ -572,7 +570,7 @@ abstract mixin class _$DeviceInfoCopyWith<$Res> implements $DeviceInfoCopyWith<$
factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl; factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
}); });
@ -589,7 +587,7 @@ class __$DeviceInfoCopyWithImpl<$Res>
/// Create a copy of DeviceInfo /// Create a copy of DeviceInfo
/// with the given fields replaced by the non-null parameter values. /// 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? isOnline = 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? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
return _then(_DeviceInfo( return _then(_DeviceInfo(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
@ -598,8 +596,7 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable 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?,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,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // 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,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?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as String?, as String?,

View File

@ -39,7 +39,6 @@ _DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
macAddress: json['mac_address'] as String?, macAddress: json['mac_address'] as String?,
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
status: json['status'] as String? ?? 'in_stock', status: json['status'] as String? ?? 'in_stock',
isOnline: json['is_online'] as bool? ?? false,
firmwareVersion: json['firmware_version'] as String? ?? '', firmwareVersion: json['firmware_version'] as String? ?? '',
lastOnlineAt: json['last_online_at'] as String?, lastOnlineAt: json['last_online_at'] as String?,
createdAt: json['created_at'] as String?, createdAt: json['created_at'] as String?,
@ -54,7 +53,6 @@ Map<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
'mac_address': instance.macAddress, 'mac_address': instance.macAddress,
'name': instance.name, 'name': instance.name,
'status': instance.status, 'status': instance.status,
'is_online': instance.isOnline,
'firmware_version': instance.firmwareVersion, 'firmware_version': instance.firmwareVersion,
'last_online_at': instance.lastOnlineAt, 'last_online_at': instance.lastOnlineAt,
'created_at': instance.createdAt, 'created_at': instance.createdAt,

View File

@ -474,7 +474,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
await _addNewBookWithAnimation( await _addNewBookWithAnimation(
title: saveResult['title'] as String? ?? '新故事', title: saveResult['title'] as String? ?? '新故事',
content: saveResult['content'] as String? ?? '', content: saveResult['content'] as String? ?? '',
coverUrl: saveResult['cover_url'] as String? ?? '',
); );
} }
} }
@ -602,7 +601,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
_addNewBookWithAnimation( _addNewBookWithAnimation(
title: saveResult['title'] as String? ?? '新故事', title: saveResult['title'] as String? ?? '新故事',
content: saveResult['content'] as String? ?? '', content: saveResult['content'] as String? ?? '',
coverUrl: saveResult['cover_url'] as String? ?? '',
); );
} }
} }
@ -902,7 +900,7 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
); );
} }
Future<void> _addNewBookWithAnimation({String title = '新故事', String content = '', String coverUrl = ''}) async { Future<void> _addNewBookWithAnimation({String title = '新故事', String content = ''}) async {
// Find the first shelf that has space // Find the first shelf that has space
int? targetShelfId; int? targetShelfId;
for (final shelf in _shelves) { for (final shelf in _shelves) {
@ -925,7 +923,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
'title': title, 'title': title,
'content': content, 'content': content,
'shelf_id': targetShelfId, 'shelf_id': targetShelfId,
if (coverUrl.isNotEmpty) 'cover_url': coverUrl,
}); });
// Reload to get the new story // Reload to get the new story
await _loadShelves(); await _loadShelves();

View File

@ -4,7 +4,6 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show PlatformException; import 'package:flutter/services.dart' show PlatformException;
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:video_player/video_player.dart';
import '../theme/design_tokens.dart'; import '../theme/design_tokens.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../widgets/pill_progress_button.dart'; import '../widgets/pill_progress_button.dart';
@ -35,10 +34,6 @@ class _StoryDetailPageState extends State<StoryDetailPage>
bool _hasGeneratedVideo = false; bool _hasGeneratedVideo = false;
bool _isLoadingVideo = false; bool _isLoadingVideo = false;
// Video Player
VideoPlayerController? _videoController;
bool _videoInitialized = false;
// TTS uses global TTSService singleton // TTS uses global TTSService singleton
final TTSService _ttsService = TTSService.instance; final TTSService _ttsService = TTSService.instance;
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
@ -113,15 +108,6 @@ class _StoryDetailPageState extends State<StoryDetailPage>
debugPrint('durationStream error (ignored): $e'); 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 // Check if audio already exists
debugPrint('[StoryDetail] story keys: ${_currentStory.keys.toList()}'); debugPrint('[StoryDetail] story keys: ${_currentStory.keys.toList()}');
debugPrint('[StoryDetail] audio_url value: "${_currentStory['audio_url']}"'); debugPrint('[StoryDetail] audio_url value: "${_currentStory['audio_url']}"');
@ -165,33 +151,12 @@ class _StoryDetailPageState extends State<StoryDetailPage>
setState(() {}); 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 @override
void dispose() { void dispose() {
_ttsService.removeListener(_onTTSChanged); _ttsService.removeListener(_onTTSChanged);
_positionSub?.cancel(); _positionSub?.cancel();
_playerStateSub?.cancel(); _playerStateSub?.cancel();
_audioPlayer.dispose(); _audioPlayer.dispose();
_videoController?.removeListener(_onVideoChanged);
_videoController?.dispose();
_genieController?.dispose(); _genieController?.dispose();
super.dispose(); super.dispose();
} }
@ -206,8 +171,7 @@ class _StoryDetailPageState extends State<StoryDetailPage>
if (_ttsService.error != null && if (_ttsService.error != null &&
!_ttsService.isGenerating && !_ttsService.isGenerating &&
_ttsService.audioUrl == null && _ttsService.audioUrl == null) {
_ttsService.errorTitle == title) {
return TTSButtonState.error; return TTSButtonState.error;
} }
if (_ttsService.isGeneratingFor(title)) { if (_ttsService.isGeneratingFor(title)) {
@ -554,58 +518,28 @@ class _StoryDetailPageState extends State<StoryDetailPage>
); );
} }
// Not yet initialized black + spinner while video loads return Stack(
if (!_videoInitialized || _videoController == null) { alignment: Alignment.center,
return const AspectRatio( children: [
aspectRatio: 16 / 9, AspectRatio(
child: ColoredBox( aspectRatio: 16 / 9,
color: Colors.black, child: Container(
child: Center( color: Colors.black,
child: CircularProgressIndicator(color: Colors.white54, strokeWidth: 3), child: const Center(
child: Icon(Icons.videocam, color: Colors.white54, size: 48),
),
), ),
), ),
); Container(
} width: 48,
height: 48,
// Initialized: VideoPlayer shows frame 0 naturally as thumbnail when paused decoration: BoxDecoration(
final isPlaying = _videoController!.value.isPlaying; color: Colors.white.withOpacity(0.8),
shape: BoxShape.circle,
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(() {});
},
),
), ),
// Play button IgnorePointer lets taps pass through to GestureDetector below child: const Icon(Icons.play_arrow, color: Colors.black),
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),
),
),
],
),
); );
} }

View File

@ -72,7 +72,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
String buffer = ''; String buffer = '';
String? storyTitle; String? storyTitle;
String? storyContent; String? storyContent;
String storyCoverUrl = '';
await for (final chunk in response.stream.transform(utf8.decoder)) { await for (final chunk in response.stream.transform(utf8.decoder)) {
buffer += chunk; buffer += chunk;
@ -110,13 +109,9 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
case 'parsing': case 'parsing':
_updateProgress(progress / 100, '正在编制最后的魔法...'); _updateProgress(progress / 100, '正在编制最后的魔法...');
break; break;
case 'cover':
_updateProgress(progress / 100, '正在绘制故事封面...');
break;
case 'done': case 'done':
storyTitle = event['title'] as String? ?? '卡皮巴拉的故事'; storyTitle = event['title'] as String? ?? '卡皮巴拉的故事';
storyContent = event['content'] as String? ?? ''; storyContent = event['content'] as String? ?? '';
storyCoverUrl = event['cover_url'] as String? ?? '';
_updateProgress(1.0, '大功告成!'); _updateProgress(1.0, '大功告成!');
break; break;
case 'error': case 'error':
@ -147,7 +142,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
story: { story: {
'title': storyTitle, 'title': storyTitle,
'content': storyContent, 'content': storyContent,
'cover_url': storyCoverUrl,
}, },
), ),
), ),
@ -160,7 +154,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
'action': 'saved', 'action': 'saved',
'title': storyTitle, 'title': storyTitle,
'content': storyContent, 'content': storyContent,
'cover_url': storyCoverUrl,
}); });
} else { } else {
Navigator.of(context).pop(result); Navigator.of(context).pop(result);

View File

@ -29,7 +29,6 @@ class TTSService extends ChangeNotifier {
// Error // Error
String? _error; String? _error;
String? _errorTitle; // Which story the error belongs to
// Getters // Getters
bool get isGenerating => _isGenerating; bool get isGenerating => _isGenerating;
@ -40,7 +39,6 @@ class TTSService extends ChangeNotifier {
String? get completedStoryTitle => _completedStoryTitle; String? get completedStoryTitle => _completedStoryTitle;
bool get justCompleted => _justCompleted; bool get justCompleted => _justCompleted;
String? get error => _error; String? get error => _error;
String? get errorTitle => _errorTitle;
/// Check if audio is ready for a specific story. /// Check if audio is ready for a specific story.
bool hasAudioFor(String title) { bool hasAudioFor(String title) {
@ -184,7 +182,6 @@ class TTSService extends ChangeNotifier {
_isGenerating = false; _isGenerating = false;
if (_audioUrl == null) { if (_audioUrl == null) {
_error = '未获取到音频'; _error = '未获取到音频';
_errorTitle = title;
_statusMessage = '生成失败'; _statusMessage = '生成失败';
} }
notifyListeners(); notifyListeners();
@ -193,7 +190,6 @@ class TTSService extends ChangeNotifier {
_isGenerating = false; _isGenerating = false;
_progress = 0.0; _progress = 0.0;
_error = e.toString(); _error = e.toString();
_errorTitle = title;
_statusMessage = '生成失败'; _statusMessage = '生成失败';
_justCompleted = false; _justCompleted = false;
notifyListeners(); notifyListeners();
@ -216,7 +212,6 @@ class TTSService extends ChangeNotifier {
_completedStoryTitle = null; _completedStoryTitle = null;
_justCompleted = false; _justCompleted = false;
_error = null; _error = null;
_errorTitle = null;
notifyListeners(); notifyListeners();
} }
} }

View File

@ -10,7 +10,6 @@ import file_selector_macos
import flutter_blue_plus_darwin import flutter_blue_plus_darwin
import just_audio import just_audio
import shared_preferences_foundation import shared_preferences_foundation
import video_player_avfoundation
import webview_flutter_wkwebview import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -19,6 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
} }

View File

@ -232,14 +232,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
custom_lint: custom_lint:
dependency: transitive dependency: transitive
description: description:
@ -548,14 +540,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1281,46 +1265,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: vm_service:
dependency: transitive dependency: transitive
description: description:

View File

@ -67,7 +67,6 @@ dependencies:
image_picker: ^1.2.1 image_picker: ^1.2.1
just_audio: ^0.9.42 just_audio: ^0.9.42
http: ^1.2.0 http: ^1.2.0
video_player: ^2.9.2
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -1,16 +1,12 @@
#!/bin/bash #!/bin/bash
# 重新编译并启动 Flutter Web (localhost:8080) # 重新编译并启动 Flutter Web (localhost:8080)
# 杀掉占用 8080 端口的进程(强制) # 杀掉占用 8080 端口的进程
PID=$(lsof -ti:8080 2>/dev/null) PID=$(lsof -ti:8080 2>/dev/null)
if [ -n "$PID" ]; then if [ -n "$PID" ]; then
echo "正在强制停止旧进程 (PID: $PID)..." echo "正在停止旧进程 (PID: $PID)..."
kill -9 $PID 2>/dev/null kill $PID 2>/dev/null
# 等待端口真正释放(最多 5 秒) sleep 1
for i in $(seq 1 10); do
lsof -ti:8080 > /dev/null 2>&1 || break
sleep 0.5
done
fi fi
echo "正在编译并启动 Flutter Web..." echo "正在编译并启动 Flutter Web..."