Files
MaliangAINovalWriter/AINoval/lib/screens/editor/widgets/novel_settings_view.dart
2025-09-10 00:07:52 +08:00

1010 lines
36 KiB
Dart

// 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<String> availableSeries;
@override
State<NovelSettingsView> createState() => _NovelSettingsViewState();
}
class _NovelSettingsViewState extends State<NovelSettingsView> {
final _formKey = GlobalKey<FormState>();
// 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<String>(
value: currentSelectedSeries,
items: widget.availableSeries.map((String seriesName) {
// Handle "New Series" display logic if needed
return DropdownMenuItem<String>(
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<void> _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<void> _uploadCoverImage(Uint8List bytes, String fileName) async {
setState(() {
_isUploading = true;
_uploadProgress = 0.0;
_uploadError = null;
});
try {
final editorRepository = context.read<EditorRepository>();
final storageRepository = context.read<StorageRepository>();
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<void> _saveMetadata() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isSaving = true;
_saveError = null;
});
try {
final repository = context.read<EditorRepository>();
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<void> _showArchiveConfirmDialog(BuildContext context) async {
final confirmed = await showDialog<bool>(
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<void> _archiveNovel() async {
try {
final repository = context.read<EditorRepository>();
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<void> _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<bool?>(
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: <Widget>[
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: <TextSpan>[
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: <Widget>[
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<void> _deleteNovel() async {
try {
final repository = context.read<EditorRepository>();
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,
);
}
}