// import 'dart:convert'; import 'dart:typed_data'; import 'dart:io'; import 'package:ainoval/models/novel_summary.dart'; import 'package:ainoval/services/api_service/repositories/editor_repository.dart'; import 'package:ainoval/services/api_service/repositories/storage_repository.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image/image.dart' as img; import 'package:ainoval/utils/web_theme.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; // Enum to represent the different tabs enum NovelEditorTab { metadata, writing, collaboration, export } class NovelSettingsView extends StatefulWidget { const NovelSettingsView({ super.key, required this.novel, required this.onSettingsClose, this.availableSeries = const ['New Series'], }); final NovelSummary novel; final VoidCallback onSettingsClose; final List availableSeries; @override State createState() => _NovelSettingsViewState(); } class _NovelSettingsViewState extends State { final _formKey = GlobalKey(); // State for the selected tab NovelEditorTab _selectedTab = NovelEditorTab.metadata; late TextEditingController _titleController; late TextEditingController _authorController; late TextEditingController _seriesIndexController; String? _selectedSeries; bool _isUploading = false; double _uploadProgress = 0.0; String? _uploadError; String? _coverUrl; bool _isSaving = false; String? _saveError; bool _hasChanges = false; String? _selectedFileName; @override void initState() { super.initState(); _titleController = TextEditingController(text: widget.novel.title); _authorController = TextEditingController(text: widget.novel.author ?? ''); _selectedSeries = widget.novel.seriesName.isNotEmpty ? widget.novel.seriesName : (widget.availableSeries.isNotEmpty ? widget.availableSeries.first : null); _seriesIndexController = TextEditingController(text: '' /* widget.novel.seriesIndex ?? '' */); _coverUrl = widget.novel.coverUrl; _titleController.addListener(_onFieldChanged); _authorController.addListener(_onFieldChanged); _seriesIndexController.addListener(_onFieldChanged); } void _onFieldChanged() { if (!_hasChanges) { setState(() { _hasChanges = true; }); } } void _onSeriesChanged(String? newValue) { setState(() { _selectedSeries = newValue; _hasChanges = true; }); } @override void dispose() { _titleController.dispose(); _authorController.dispose(); _seriesIndexController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // final theme = Theme.of(context); return Material( child: Container( color: WebTheme.getBackgroundColor(context), // 使用主题背景色 // Use all available height if needed, or constrain it // height: double.infinity, padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 48), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1100), // Use Column to stack Navigation Bar and Content child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch content horizontally children: [ // Navigation Bar _buildNavigationBar(), const SizedBox(height: 24), // Spacing below nav bar // Content Area based on selected tab Expanded( // Use Expanded to take remaining vertical space child: SingleChildScrollView( child: _buildSelectedTabView(), ), ), ], ), ), ), ), ); } // Builds the top navigation bar Widget _buildNavigationBar() { return Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : WebTheme.grey300, width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.start, // Align buttons to the left children: [ _buildNavButton(NovelEditorTab.metadata, '元数据', Icons.info_outline), _buildNavButton(NovelEditorTab.writing, '写作', Icons.edit_note), _buildNavButton(NovelEditorTab.collaboration, '协作', Icons.people_outline), _buildNavButton(NovelEditorTab.export, '导出', Icons.upload_file_outlined), ], ), ); } // Helper to build individual navigation buttons Widget _buildNavButton(NovelEditorTab tab, String label, IconData icon) { final bool isSelected = _selectedTab == tab; final theme = Theme.of(context); final Color activeColor = WebTheme.getPrimaryColor(context); // Or your desired active color final Color inactiveColor = theme.colorScheme.onSurfaceVariant; return Material( color: Colors.transparent, child: InkWell( onTap: () { setState(() { _selectedTab = tab; }); }, splashColor: WebTheme.getPrimaryColor(context).withOpacity(0.1), highlightColor: WebTheme.getPrimaryColor(context).withOpacity(0.05), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18, color: isSelected ? activeColor : inactiveColor), const SizedBox(width: 8), Text( label, style: TextStyle( color: isSelected ? activeColor : inactiveColor, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, fontSize: 14, // Adjust font size if needed ), ), ], ), ), ), ); } // Builds the content view based on the selected tab Widget _buildSelectedTabView() { switch (_selectedTab) { case NovelEditorTab.metadata: return _buildMetadataSettingsView(); // Return the original settings content case NovelEditorTab.writing: return const Center(child: Text('写作 界面 (待开发)')); // Placeholder case NovelEditorTab.collaboration: return const Center(child: Text('协作 界面 (待开发)')); // Placeholder case NovelEditorTab.export: return const Center(child: Text('导出 界面 (待开发)')); // Placeholder } } // Extracted the original settings content into its own builder method Widget _buildMetadataSettingsView() { final theme = Theme.of(context); final textTheme = theme.textTheme; // Row containing the card-styled metadata form and cover preview return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // --- Left Column: Metadata Form and Danger Zone --- Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // --- Metadata Card --- _buildCard( child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'METADATA', style: textTheme.labelSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurfaceVariant, letterSpacing: 1.2, ), ), Text( '这是您小说的元数据,用于整理您的小说集锦。', // Chinese style: textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 24), _buildLabeledTextField( controller: _titleController, label: '小说标题', // Chinese required: true, ), const SizedBox(height: 20), _buildLabeledTextField( controller: _authorController, label: '作者 / 笔名', // Chinese ), const SizedBox(height: 20), _buildSeriesInput(), // Contains Chinese text inside const SizedBox(height: 32), Row( children: [ ElevatedButton( onPressed: _hasChanges && !_isSaving ? _saveMetadata : null, style: ElevatedButton.styleFrom( backgroundColor: WebTheme.getPrimaryColor(context), foregroundColor: WebTheme.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), textStyle: textTheme.labelLarge, ), child: _isSaving ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2.5, color: theme.colorScheme.onPrimary) ) : const Text('保存更改'), // Chinese ), const SizedBox(width: 12), TextButton( onPressed: widget.onSettingsClose, // This button might navigate away entirely now child: const Text('取消'), // Chinese style: TextButton.styleFrom( foregroundColor: WebTheme.getSecondaryTextColor(context), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), textStyle: textTheme.labelLarge, ) ), ], ), if (_saveError != null) Padding( padding: const EdgeInsets.only(top: 12), child: Text( _saveError!, // Error messages likely still in English from backend style: TextStyle( color: theme.colorScheme.error, fontSize: 13, ), ), ), ], ), ), ), const SizedBox(height: 24), // Spacing between cards // --- Danger Zone Card --- _buildCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'DANGER ZONE', style: textTheme.labelSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurfaceVariant, letterSpacing: 1.2, ), ), Text( '本节中的某些操作无法撤销,并可能产生意想不到的后果。', // Chinese style: textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 20), Row( children: [ TextButton.icon( onPressed: () => _showArchiveConfirmDialog(context), icon: const Icon(Icons.archive_outlined, size: 18), label: const Text('归档小说'), // Chinese style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), ), const SizedBox(width: 16), TextButton.icon( onPressed: () => _showDeleteConfirmDialog(context), icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.error), label: Text('删除小说', style: TextStyle(color: theme.colorScheme.error)), // Chinese style: TextButton.styleFrom( foregroundColor: theme.colorScheme.error, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), ), ], ), const SizedBox(height: 16), // Bottom padding inside card ], ), ), ], ), ), const SizedBox(width: 48), // --- Right Column: Cover Card --- Expanded( flex: 2, // Wrap the cover section in a card child: _buildCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'COVER', style: textTheme.labelSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurfaceVariant, letterSpacing: 1.2, ), ), Text( '这是您小说的封面。它将显示在小说集锦页面上。', // Chinese style: textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 12), Text( '上传你的封面', // Chinese style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), ), Text( '或将文件拖放到此区域', // Chinese style: textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), // Wrap InkWell with AspectRatio for better responsive height? // Or keep fixed height if design requires it. InkWell( onTap: _isUploading ? null : _selectCoverImage, borderRadius: BorderRadius.circular(8), child: Container( height: 350, // Keep fixed height as per previous design width: double.infinity, decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(8), border: Border.all( color: _uploadError != null ? theme.colorScheme.error : theme.colorScheme.outlineVariant, width: 1, ), ), child: _buildCoverPreview(), // Cover preview logic remains the same ), ), if (_isUploading) Padding( padding: const EdgeInsets.only(top: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('上传中 ${_selectedFileName ?? '图片'}...', style: textTheme.bodySmall), // Chinese const SizedBox(height: 4), LinearProgressIndicator( value: _uploadProgress, minHeight: 6, borderRadius: BorderRadius.circular(3), ), ], ) ) ], ), ), // End Card ), ], ); } Widget _buildLabeledTextField({ required TextEditingController controller, required String label, String? hint, bool required = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label + (required ? ' *' : ''), style: Theme.of(context).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), ), const SizedBox(height: 8), TextFormField( controller: controller, decoration: WebTheme.getBorderedInputDecoration( hintText: hint, context: context, ), validator: required ? (value) => value == null || value.isEmpty // Use label in error message ? '$label 不能为空' // Chinese : null : null, style: Theme.of(context).textTheme.bodyMedium, ), ], ); } Widget _buildSeriesInput() { final theme = Theme.of(context); final textTheme = theme.textTheme; final currentSelectedSeries = widget.availableSeries.contains(_selectedSeries) ? _selectedSeries : (widget.availableSeries.isNotEmpty ? widget.availableSeries.first : null); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '系列 (可选)', // Chinese style: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: DropdownButtonFormField( value: currentSelectedSeries, items: widget.availableSeries.map((String seriesName) { // Handle "New Series" display logic if needed return DropdownMenuItem( value: seriesName, child: Text(seriesName == 'New Series' ? '新建系列' : seriesName), // Example Chinese display ); }).toList(), onChanged: _onSeriesChanged, decoration: WebTheme.getBorderedInputDecoration( context: context, ), style: textTheme.bodyMedium, ), ), const SizedBox(width: 12), Expanded( flex: 1, child: TextFormField( controller: _seriesIndexController, decoration: WebTheme.getBorderedInputDecoration( hintText: '系列索引 (例如:卷一)', // Chinese hint context: context, ), style: textTheme.bodyMedium, ), ), ], ), ], ); } Widget _buildCoverPreview() { final theme = Theme.of(context); final textTheme = theme.textTheme; if (_isUploading && _uploadProgress < 0.9) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(value: _uploadProgress > 0.1 ? _uploadProgress : null), const SizedBox(height: 16), Text('上传中...', style: textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface)), if (_selectedFileName != null) Text(_selectedFileName!, style: textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), overflow: TextOverflow.ellipsis), ], ), ), ); } if (_coverUrl != null && _coverUrl!.isNotEmpty) { return Stack( fit: StackFit.expand, children: [ ClipRRect( borderRadius: BorderRadius.circular(7.0), child: Image.network( _coverUrl!, fit: BoxFit.contain, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center(child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, )); }, errorBuilder: (context, error, stackTrace) { return _buildUploadPlaceholder(isError: true); }, ), ), Positioned( top: 8, right: 8, child: IconButton( onPressed: _selectCoverImage, icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), shape: BoxShape.circle, ), child: Icon( Icons.edit, color: WebTheme.white, size: 16, ), ), tooltip: '修改封面', constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), ), ], ); } return _buildUploadPlaceholder(); } Widget _buildUploadPlaceholder({bool isError = false}) { final color = isError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.onSurfaceVariant; return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isError ? Icons.error_outline : Icons.cloud_upload_outlined, size: 56, color: color, ), const SizedBox(height: 16), Text( isError ? '封面加载失败' : '上传封面', // Chinese textAlign: TextAlign.center, style: TextStyle( fontSize: 15, color: color, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 4), Text( isError ? '请重试上传.' : '或拖放到此区域', // Chinese textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), if (!isError) ...[ const SizedBox(height: 12), Text( '支持 JPG, PNG, GIF, WEBP 格式\n建议尺寸: 600x900 像素', // Chinese textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3 ), ), ] ], ), ), ); } Future _selectCoverImage() async { setState(() { _uploadError = null; _selectedFileName = null; }); try { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result != null && result.files.isNotEmpty) { final file = result.files.first; final allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; final fileExtension = file.extension?.toLowerCase(); if (fileExtension == null || !allowedExtensions.contains(fileExtension)) { throw Exception('无效的文件类型。请选择 JPG, PNG, GIF 或 WEBP。'); // Chinese } setState(() { _selectedFileName = file.name; }); Uint8List fileBytes; if (file.bytes != null) { fileBytes = file.bytes!; } else if (file.path != null) { final File imageFile = File(file.path!); fileBytes = await imageFile.readAsBytes(); } else { throw Exception('无法读取所选图片文件。'); // Chinese } final img.Image? image = img.decodeImage(fileBytes); if (image == null) { throw Exception('无法解码所选图片。'); // Chinese } img.Image resizedImage = image; const maxSize = 1200; if (image.width > maxSize || image.height > maxSize) { resizedImage = img.copyResize( image, width: image.width > image.height ? maxSize : null, height: image.height >= image.width ? maxSize : null, interpolation: img.Interpolation.average, ); } final compressedBytes = img.encodeJpg(resizedImage, quality: 85); final timestamp = DateTime.now().millisecondsSinceEpoch; final uniqueFileName = '${widget.novel.id}_${timestamp}_cover.jpg'; await _uploadCoverImage(Uint8List.fromList(compressedBytes), uniqueFileName); } else { AppLogger.i('NovelSettingsView', 'User cancelled file selection.'); } } catch (e, stackTrace) { AppLogger.e('NovelSettingsView', 'Error selecting/processing cover image', e, stackTrace); if (mounted) { final errorMessage = e is Exception ? e.toString().replaceFirst('Exception: ', '') : e.toString(); setState(() { _uploadError = errorMessage; _isUploading = false; }); TopToast.error(context, '错误: $errorMessage'); } } } Future _uploadCoverImage(Uint8List bytes, String fileName) async { setState(() { _isUploading = true; _uploadProgress = 0.0; _uploadError = null; }); try { final editorRepository = context.read(); final storageRepository = context.read(); await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; setState(() => _uploadProgress = 0.1); final coverUrl = await storageRepository.uploadCoverImage( novelId: widget.novel.id, fileBytes: bytes, fileName: fileName, ); if (!mounted) return; setState(() => _uploadProgress = 0.8); await editorRepository.updateNovelCover( novelId: widget.novel.id, coverUrl: coverUrl, ); if (!mounted) return; setState(() { _coverUrl = coverUrl; _uploadProgress = 1.0; }); await Future.delayed(const Duration(milliseconds: 500)); if (!mounted) return; setState(() { _isUploading = false; _selectedFileName = null; _hasChanges = false; }); TopToast.success(context, '封面上传成功!'); } catch (e, stackTrace) { AppLogger.e('NovelSettingsView', 'Failed to upload cover image', e, stackTrace); if (mounted) { final errorMessage = e is Exception ? e.toString().replaceFirst('Exception: ', '') : e.toString(); setState(() { _isUploading = false; _uploadError = errorMessage; _uploadProgress = 0.0; }); TopToast.error(context, '上传失败: $errorMessage'); } } } Future _saveMetadata() async { if (!_formKey.currentState!.validate()) { return; } setState(() { _isSaving = true; _saveError = null; }); try { final repository = context.read(); await repository.updateNovelMetadata( novelId: widget.novel.id, title: _titleController.text.trim(), author: _authorController.text.trim(), series: (_selectedSeries != null && _selectedSeries != 'New Series') ? _selectedSeries : null, // TODO: Update EditorRepository.updateNovelMetadata to accept seriesIndex // seriesIndex: _seriesIndexController.text.trim(), // Save index ); if (mounted) { setState(() { _isSaving = false; _hasChanges = false; }); TopToast.success(context, '小说元数据已更新.'); // 关闭设置页面,返回编辑器 widget.onSettingsClose(); } } catch (e, stackTrace) { AppLogger.e('NovelSettingsView', 'Failed to save metadata', e, stackTrace); if (mounted) { final errorMessage = e is Exception ? e.toString().replaceFirst('Exception: ', '') : e.toString(); setState(() { _isSaving = false; _saveError = '保存失败: $errorMessage'; // Keep backend error potentially English }); TopToast.error(context, '保存失败: $errorMessage'); } } } Future _showArchiveConfirmDialog(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon(Icons.archive_outlined, color: WebTheme.getPrimaryColor(context)), const SizedBox(width: 8), const Text('确认归档'), // Chinese ], ), content: const Text( '归档操作会将小说从您的主列表中隐藏。您可以稍后取消归档。确定要归档这本小说吗?' // Chinese ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), // Chinese ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: WebTheme.getPrimaryColor(context), foregroundColor: WebTheme.white, ), child: const Text('确认归档'), // Chinese ), ], ), ); if (confirmed == true) { _archiveNovel(); } } Future _archiveNovel() async { try { final repository = context.read(); await repository.archiveNovel(novelId: widget.novel.id); if (mounted) { TopToast.success(context, '小说已成功归档。'); widget.onSettingsClose(); // Close or navigate back } } catch (e, stackTrace) { AppLogger.e('NovelSettingsView', 'Failed to archive novel', e, stackTrace); if (mounted) { final errorMessage = e is Exception ? e.toString().replaceFirst('Exception: ', '') : e.toString(); TopToast.error(context, '归档失败: $errorMessage'); } } } Future _showDeleteConfirmDialog(BuildContext context) async { final theme = Theme.of(context); final novelTitle = _titleController.text.trim(); final TextEditingController confirmController = TextEditingController(); bool isConfirmed = false; final confirmedResult = await showDialog( context: context, barrierDismissible: false, builder: (context) { return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( title: Row( children: [ Icon(Icons.warning_amber_rounded, color: theme.colorScheme.error), const SizedBox(width: 8), const Text('永久删除'), // Chinese ], ), content: SingleChildScrollView( child: ListBody( children: [ const Text( '警告:此操作无法撤销!', // Chinese style: TextStyle(fontWeight: FontWeight.bold, color: WebTheme.error), ), const SizedBox(height: 16), const Text( '删除这本小说将永久移除其所有内容、章节和设置。这些数据将无法恢复。', // Chinese ), const SizedBox(height: 16), RichText( text: TextSpan( style: DefaultTextStyle.of(context).style, children: [ const TextSpan(text: '请输入小说标题 '), // Chinese TextSpan(text: '"$novelTitle"', style: const TextStyle(fontWeight: FontWeight.bold)), const TextSpan(text: ' 以确认删除:'), // Chinese ], ), ), const SizedBox(height: 8), TextField( controller: confirmController, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: '输入 "$novelTitle"', // Chinese errorText: !isConfirmed && confirmController.text.isNotEmpty && confirmController.text != novelTitle ? '标题不匹配' // Chinese : null, ), autofocus: true, onChanged: (value) { setDialogState(() { isConfirmed = value == novelTitle; }); }, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), // Chinese ), ElevatedButton( onPressed: isConfirmed ? () { Navigator.pop(context, true); } : null, style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.error, foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey.shade300, ), child: const Text('确认删除'), // Chinese ), ], ); } ); } ); confirmController.dispose(); if (confirmedResult == true) { _deleteNovel(); } } Future _deleteNovel() async { try { final repository = context.read(); await repository.deleteNovel(novelId: widget.novel.id); if (mounted) { TopToast.success(context, '小说已永久删除。'); widget.onSettingsClose(); // Close or navigate back } } catch (e, stackTrace) { AppLogger.e('NovelSettingsView', 'Failed to delete novel', e, stackTrace); if (mounted) { final errorMessage = e is Exception ? e.toString().replaceFirst('Exception: ', '') : e.toString(); TopToast.error(context, '删除失败: $errorMessage'); } } } // Helper method to build a styled card Container Widget _buildCard({required Widget child}) { return Container( padding: const EdgeInsets.all(24.0), decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(8.0), border: Border.all(color: WebTheme.getBorderColor(context), width: 1.0), // Optional: Add a subtle shadow // boxShadow: [ // BoxShadow( // color: Colors.grey.withOpacity(0.1), // spreadRadius: 1, // blurRadius: 3, // offset: Offset(0, 1), // changes position of shadow // ), // ], ), child: child, ); } }