generated from dellevin/template
云备份界面优化
This commit is contained in:
@@ -580,6 +580,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
'read' => ('已读', colors.primary, colors.onPrimary),
|
||||
'reading' => ('在读', colors.outlineVariant, colors.onSurface.withValues(alpha: 0.6)),
|
||||
'want_to_read' => ('想读', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.4)),
|
||||
'abandoned' => ('弃读', colors.error.withValues(alpha: 0.15), colors.error),
|
||||
_ => ('', colors.surfaceContainerHighest, colors.onSurface.withValues(alpha: 0.3)),
|
||||
};
|
||||
if (label.isEmpty) return const SizedBox.shrink();
|
||||
@@ -702,6 +703,11 @@ class _BookDetailPageState extends State<BookDetailPage> {
|
||||
bgColor = colors.surfaceContainerHighest;
|
||||
textColor = colors.onSurface.withValues(alpha: 0.4);
|
||||
break;
|
||||
case 'abandoned':
|
||||
label = '弃读';
|
||||
bgColor = colors.error.withValues(alpha: 0.15);
|
||||
textColor = colors.error;
|
||||
break;
|
||||
default:
|
||||
label = '未知';
|
||||
bgColor = colors.outlineVariant;
|
||||
|
||||
@@ -266,6 +266,7 @@ class _BookFormPageState extends State<BookFormPage> {
|
||||
_buildStatusOption('想读', 'want_to_read'),
|
||||
_buildStatusOption('在读', 'reading'),
|
||||
_buildStatusOption('已读', 'read'),
|
||||
_buildStatusOption('弃读', 'abandoned'),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -44,7 +44,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||
static const _statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read', 3: 'abandoned'};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -166,7 +166,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
final direction = (velocity ?? 0) > 0 ? -1 : 1; // 右滑→上一个,左滑→下一个
|
||||
final provider = context.read<AppProvider>();
|
||||
final currentIndex = provider.bookStatusIndex;
|
||||
final newIndex = (currentIndex + direction + 3) % 3;
|
||||
final newIndex = (currentIndex + direction + 4) % 4;
|
||||
setState(() => _dragDelta = 0.0);
|
||||
provider.setBookStatusIndex(newIndex);
|
||||
},
|
||||
@@ -297,7 +297,7 @@ class _BookTabPageState extends State<BookTabPage> {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final provider = context.read<AppProvider>();
|
||||
final isWallMode = provider.bookshelfMode;
|
||||
final statusText = isWallMode ? '' : ['已读', '在读', '想读'][statusIndex];
|
||||
final statusText = isWallMode ? '' : ['已读', '在读', '想读', '弃读'][statusIndex];
|
||||
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Container(width: 80, height: 80,
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
|
||||
|
||||
@@ -24,6 +24,15 @@ class CloudSyncPage extends StatelessWidget {
|
||||
onTap: () =>
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildOption(
|
||||
colors: colors,
|
||||
icon: Icons.cloud_sync_outlined,
|
||||
title: '服务器云同步',
|
||||
subtitle: '自建云同步服务器,多端同步更改',
|
||||
onTap: () {},
|
||||
enabled: false,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildInfo(colors),
|
||||
],
|
||||
|
||||
@@ -21,10 +21,13 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
bool _isLoading = false;
|
||||
bool _isConfigured = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isAutoSyncEnabled = false;
|
||||
int _autoSyncInterval = 5;
|
||||
SyncDirection _syncDirection = SyncDirection.upload;
|
||||
|
||||
// 远程备份信息
|
||||
DateTime? _remoteModifiedTime;
|
||||
int? _remoteFileSize;
|
||||
bool _isLoadingRemoteInfo = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -50,14 +53,24 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
_pathController.text = config['path'] ?? '/mooknote';
|
||||
_isConfigured = true;
|
||||
});
|
||||
_loadRemoteInfo();
|
||||
}
|
||||
|
||||
final autoSyncEnabled = await WebDAVService.instance.isAutoSyncEnabled();
|
||||
final autoSyncInterval = await WebDAVService.instance.getAutoSyncInterval();
|
||||
setState(() {
|
||||
_isAutoSyncEnabled = autoSyncEnabled;
|
||||
_autoSyncInterval = autoSyncInterval;
|
||||
});
|
||||
debugPrint('WebDAV auto sync: enabled=$autoSyncEnabled, interval=$autoSyncInterval');
|
||||
}
|
||||
|
||||
Future<void> _loadRemoteInfo() async {
|
||||
setState(() => _isLoadingRemoteInfo = true);
|
||||
final info = await WebDAVService.instance.getRemoteBackupInfo();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_remoteModifiedTime = info?['modifiedTime'] as DateTime?;
|
||||
_remoteFileSize = info?['size'] as int?;
|
||||
_isLoadingRemoteInfo = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveConfig() async {
|
||||
@@ -118,6 +131,8 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.success) {
|
||||
// 同步成功后刷新远程文件信息
|
||||
_loadRemoteInfo();
|
||||
final details = '上传: ${result.uploadedFiles} 文件, ${result.uploadedImages} 图片\n'
|
||||
'下载: ${result.downloadedFiles} 文件, ${result.downloadedImages} 图片';
|
||||
|
||||
@@ -189,76 +204,79 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleAutoSync(bool value) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
if (value) {
|
||||
await WebDAVService.instance.startAutoSync();
|
||||
if (mounted) ToastUtil.show(context, '自动同步已开启,每 $_autoSyncInterval 分钟同步一次');
|
||||
} else {
|
||||
await WebDAVService.instance.stopAutoSync();
|
||||
if (mounted) ToastUtil.show(context, '自动同步已关闭');
|
||||
}
|
||||
setState(() => _isAutoSyncEnabled = value);
|
||||
} catch (e) {
|
||||
if (mounted) ToastUtil.show(context, '设置失败: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showIntervalPicker() async {
|
||||
final intervals = [1, 3, 5, 10, 15, 30, 60];
|
||||
final selected = await showModalBottomSheet<int>(
|
||||
Future<void> _showUploadConfirm() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 12),
|
||||
Text('同步间隔',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const SizedBox(height: 8),
|
||||
...intervals.map((i) => ListTile(
|
||||
title: Text('$i 分钟',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: _autoSyncInterval == i ? FontWeight.w600 : FontWeight.w400,
|
||||
color: colors.onSurface)),
|
||||
trailing: _autoSyncInterval == i
|
||||
? Icon(Icons.check, color: colors.primary, size: 20)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(ctx, i),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('确认上传',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('该操作会覆盖掉远程数据,请谨慎操作,点击确定继续上传',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == true) {
|
||||
setState(() => _syncDirection = SyncDirection.upload);
|
||||
_syncData();
|
||||
}
|
||||
}
|
||||
|
||||
if (selected != null && selected != _autoSyncInterval) {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await WebDAVService.instance.setAutoSyncInterval(selected);
|
||||
setState(() => _autoSyncInterval = selected);
|
||||
if (mounted) ToastUtil.show(context, '同步间隔已设置为 $selected 分钟');
|
||||
} catch (e) {
|
||||
if (mounted) ToastUtil.show(context, '设置失败: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
Future<void> _showDownloadConfirm() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('确认下载',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('该操作会拉取远程数据覆盖掉本地数据,请谨慎操作,点击确定将数据拉取到本地',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == true) {
|
||||
setState(() => _syncDirection = SyncDirection.download);
|
||||
_syncData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +343,6 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
_passwordController.clear();
|
||||
_pathController.text = '/mooknote';
|
||||
_isConfigured = false;
|
||||
_isAutoSyncEnabled = false;
|
||||
});
|
||||
if (mounted) ToastUtil.show(context, '配置已清除');
|
||||
}
|
||||
@@ -339,18 +356,18 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(title: const Text('WebDAV 备份')),
|
||||
body: _isLoading && !_isConfigured
|
||||
? Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
body: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 已连接提示
|
||||
if (_isConfigured) _buildConnectedBanner(colors),
|
||||
// 已连接提示
|
||||
if (_isConfigured) _buildConnectedBanner(colors),
|
||||
|
||||
// 服务器配置
|
||||
_buildSectionLabel(colors, '服务器配置'),
|
||||
// 服务器配置
|
||||
_buildSectionLabel(colors, '服务器配置'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput(
|
||||
colors: colors,
|
||||
@@ -391,61 +408,39 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
|
||||
// 测试并保存
|
||||
_buildBtn(colors, '测试并保存', onTap: _isLoading ? null : _saveConfig),
|
||||
if (_isConfigured) ...[
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: _isLoading ? null : _clearConfig,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text('清除配置',
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
|
||||
if (_isConfigured) ...[
|
||||
// 手动同步
|
||||
_buildSectionLabel(colors, '手动同步'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDirectionChip(
|
||||
colors, '上传到云端', SyncDirection.upload, Icons.upload)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDirectionChip(
|
||||
colors, '下载到本地', SyncDirection.download, Icons.download)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildBtn(colors, '立即同步', onTap: _isLoading ? null : _syncData, loading: _isLoading),
|
||||
|
||||
// 远程备份信息卡片(包含操作按钮)
|
||||
_buildRemoteInfoCard(colors),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 自动同步
|
||||
_buildSectionLabel(colors, '自动同步'),
|
||||
const SizedBox(height: 10),
|
||||
_buildSwitchRow(
|
||||
colors: colors,
|
||||
icon: Icons.sync,
|
||||
label: '自动同步',
|
||||
sub: _isAutoSyncEnabled ? '每 $_autoSyncInterval 分钟自动同步' : '关闭',
|
||||
value: _isAutoSyncEnabled,
|
||||
onChanged: _isLoading ? null : _toggleAutoSync,
|
||||
),
|
||||
if (_isAutoSyncEnabled) ...[
|
||||
const SizedBox(height: 4),
|
||||
_buildTappableRow(
|
||||
colors: colors,
|
||||
label: '同步间隔',
|
||||
value: '$_autoSyncInterval 分钟',
|
||||
onTap: _isLoading ? null : _showIntervalPicker,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 清除配置
|
||||
_buildTextBtn(colors, '清除配置', onTap: _isLoading ? null : _clearConfig),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildTips(colors),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
_buildTips(colors),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
if (_isLoading)
|
||||
Container(
|
||||
color: colors.surface.withValues(alpha: 0.7),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,126 +553,92 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextBtn(ColorScheme colors, String text, {VoidCallback? onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: onTap == null
|
||||
? colors.onSurface.withValues(alpha: 0.25)
|
||||
: colors.onSurface.withValues(alpha: 0.4))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildRemoteInfoCard(ColorScheme colors) {
|
||||
String timeText;
|
||||
String sizeText = '';
|
||||
|
||||
if (_isLoadingRemoteInfo) {
|
||||
timeText = '加载中...';
|
||||
} else if (_remoteModifiedTime != null) {
|
||||
final dt = _remoteModifiedTime!;
|
||||
timeText = '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
if (_remoteFileSize != null) {
|
||||
sizeText = _formatFileSize(_remoteFileSize!);
|
||||
}
|
||||
} else {
|
||||
timeText = '暂无备份文件';
|
||||
}
|
||||
|
||||
Widget _buildSwitchRow({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String sub,
|
||||
required bool value,
|
||||
required ValueChanged<bool>? onChanged,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.5), width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 18,
|
||||
color: value ? colors.primary : colors.onSurface.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ? sub : label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: value ? colors.onSurface.withValues(alpha: 0.6) : colors.onSurface),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_outlined, size: 18, color: colors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('云端备份', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
const Spacer(),
|
||||
if (_isLoadingRemoteInfo)
|
||||
SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeThumbColor: colors.primary,
|
||||
activeTrackColor: colors.primary.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: colors.surface,
|
||||
inactiveTrackColor: colors.onSurface.withValues(alpha: 0.15),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('上传时间', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 4),
|
||||
Text(timeText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (sizeText.isNotEmpty)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('文件大小', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const SizedBox(height: 4),
|
||||
Text(sizeText, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 0.5, color: Color(0xFFE0E0E0)),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBtn(colors, '上传', onTap: _isLoading ? null : () => _showUploadConfirm(), loading: _isLoading),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildBtn(colors, '下载', onTap: _isLoading ? null : () => _showDownloadConfirm(), loading: _isLoading),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTappableRow({
|
||||
required ColorScheme colors,
|
||||
required String label,
|
||||
required String value,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 28),
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
const Spacer(),
|
||||
Text(value,
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.chevron_right,
|
||||
size: 16, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDirectionChip(ColorScheme colors, String label, SyncDirection dir, IconData icon) {
|
||||
final selected = _syncDirection == dir;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _syncDirection = dir),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? colors.primary : colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: selected
|
||||
? colors.onPrimary
|
||||
: colors.onSurface.withValues(alpha: 0.4)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: selected
|
||||
? colors.onPrimary
|
||||
: colors.onSurface.withValues(alpha: 0.6))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '${bytes}B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB';
|
||||
}
|
||||
|
||||
Widget _buildTips(ColorScheme colors) {
|
||||
|
||||
@@ -490,7 +490,57 @@ class WebDAVService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取远程文件的修改时间
|
||||
/// 获取远程备份文件信息(修改时间和大小)
|
||||
Future<Map<String, dynamic>?> getRemoteBackupInfo() async {
|
||||
final config = await getConfig();
|
||||
if (config == null) return null;
|
||||
|
||||
final url = config['url']!;
|
||||
final username = config['username']!;
|
||||
final password = config['password']!;
|
||||
final path = config['path']!;
|
||||
|
||||
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
||||
final zipUrl = '$baseUrl$path/mooknote_backup.zip';
|
||||
|
||||
final client = http.Client();
|
||||
try {
|
||||
var request = http.Request('HEAD', Uri.parse(zipUrl));
|
||||
request.headers['Authorization'] = _basicAuth(username, password);
|
||||
|
||||
var response = await client.send(request);
|
||||
|
||||
// 处理重定向
|
||||
if (response.statusCode == 301 || response.statusCode == 302 ||
|
||||
response.statusCode == 307 || response.statusCode == 308) {
|
||||
final location = response.headers['location'];
|
||||
if (location != null) {
|
||||
request = http.Request('HEAD', Uri.parse(location));
|
||||
request.headers['Authorization'] = _basicAuth(username, password);
|
||||
response = await client.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final lastModified = response.headers['last-modified'];
|
||||
final contentLength = response.headers['content-length'];
|
||||
DateTime? modifiedTime;
|
||||
if (lastModified != null) {
|
||||
modifiedTime = HttpDate.parse(lastModified).toLocal();
|
||||
}
|
||||
return {
|
||||
'modifiedTime': modifiedTime,
|
||||
'size': contentLength != null ? int.tryParse(contentLength) : null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('[WebDAV] 获取远程备份信息失败: $e');
|
||||
return null;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
Future<DateTime?> _getRemoteFileModifiedTime(
|
||||
http.Client client,
|
||||
String url,
|
||||
|
||||
@@ -13,7 +13,7 @@ void showQuickAddSheet(BuildContext context, AppProvider provider) {
|
||||
Navigator.pushNamed(context, '/movie-form',
|
||||
arguments: {'initialStatus': currentStatus});
|
||||
case 1:
|
||||
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read'};
|
||||
final statusMap = {0: 'read', 1: 'reading', 2: 'want_to_read', 3: 'abandoned'};
|
||||
final currentStatus =
|
||||
statusMap[provider.bookStatusIndex] ?? 'want_to_read';
|
||||
Navigator.pushNamed(context, '/book-form',
|
||||
@@ -93,6 +93,7 @@ void showAddSheet(BuildContext context, AppProvider provider) {
|
||||
0: 'read',
|
||||
1: 'reading',
|
||||
2: 'want_to_read',
|
||||
3: 'abandoned',
|
||||
};
|
||||
final s =
|
||||
statusMap[provider.bookStatusIndex] ?? 'want_to_read';
|
||||
|
||||
@@ -26,7 +26,7 @@ class BookStatusBar extends StatelessWidget {
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final tabWidth = constraints.maxWidth / 3;
|
||||
final tabWidth = constraints.maxWidth / 4;
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Stack(
|
||||
@@ -57,6 +57,8 @@ class BookStatusBar extends StatelessWidget {
|
||||
() => provider.setBookStatusIndex(1)),
|
||||
_buildTab(colors, '想读', Icons.bookmark_outlined, currentIndex == 2,
|
||||
() => provider.setBookStatusIndex(2)),
|
||||
_buildTab(colors, '弃读', Icons.cancel_outlined, currentIndex == 3,
|
||||
() => provider.setBookStatusIndex(3)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -39,75 +39,53 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
},
|
||||
onLongPress: () => _showActions(context),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: selected ? Border.all(color: colors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 标题(带置顶图标)
|
||||
if (note.title.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
if (note.isPinned)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(Icons.push_pin, size: 14, color: colors.primary),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
note.title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// 标题行(带置顶图标 + 时间)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (note.isPinned)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(Icons.push_pin, size: 14, color: colors.primary),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
_formatDate(note.createdAt),
|
||||
Expanded(
|
||||
child: Text(
|
||||
note.title.isNotEmpty ? note.title : previewText.isNotEmpty ? previewText : '(无内容)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
_formatDate(note.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 内容预览(无标题时才显示置顶和时间,替换原本只有置顶的逻辑)
|
||||
// 内容预览
|
||||
if (previewText.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
if (note.title.isEmpty)
|
||||
Row(
|
||||
children: [
|
||||
if (note.isPinned)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(Icons.push_pin, size: 14, color: colors.primary),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
_formatDate(note.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (note.title.isEmpty)
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
previewText,
|
||||
style: TextStyle(
|
||||
@@ -115,12 +93,12 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
color: colors.onSurface.withValues(alpha: 0.5),
|
||||
height: 1.5,
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLines: note.title.isNotEmpty ? 2 : 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
||||
// 标签(换行显示)
|
||||
// 标签
|
||||
if (note.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
|
||||
Reference in New Issue
Block a user