1.3.21.3.2
Android APK Release / Build Android APK (push) Successful in 55m29s

This commit is contained in:
2026-05-26 16:32:18 +08:00
parent 546cef5ba6
commit 17673b2862
89 changed files with 24098 additions and 272 deletions
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/utils/file_utils.dart';
/// 面包屑导航组件
@@ -15,7 +16,7 @@ class FileBreadcrumb extends StatelessWidget {
@override
Widget build(BuildContext context) {
final pathParts = FileUtils.toRelativePath(currentPath).split('/');
final pathParts = currentPath.split('/');
pathParts.removeWhere((part) => part.isEmpty);
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
@@ -55,7 +56,7 @@ class FileBreadcrumb extends StatelessWidget {
),
_buildBreadcrumbItem(
context,
name: FileUtils.decodePathSegment(pathParts[i]),
name: _decodePathSegment(pathParts[i]),
path: '/${pathParts.sublist(0, i + 1).join('/')}',
icon: null,
primaryColor: colorScheme.primary,
@@ -69,6 +70,10 @@ class FileBreadcrumb extends StatelessWidget {
);
}
String _decodePathSegment(String value) {
return FileUtils.safeDecodePathSegment(value);
}
Widget _buildBreadcrumbItem(
BuildContext context, {
required String name,
@@ -210,7 +210,7 @@ class _FileInfoPanelState extends State<FileInfoPanel> {
_buildInfoRow(
LucideIcons.folderOpen,
'位置',
FileUtils.decodePathForDisplay(file.relativePath),
FileUtils.safeDecodePathSegment(file.relativePath),
),
if (file.isFile)
_buildInfoRow(
+12 -26
View File
@@ -2,7 +2,6 @@ import 'package:cloudreve4_flutter/data/models/file_model.dart';
import 'package:cloudreve4_flutter/services/file_service.dart';
import 'package:flutter/material.dart';
import '../../core/utils/app_logger.dart';
import '../../core/utils/file_utils.dart';
/// 文件夹选择器
class FolderPicker extends StatefulWidget {
@@ -62,7 +61,7 @@ class _FolderPickerState extends State<FolderPicker> {
void _enterFolder(FileModel folder) {
setState(() {
_currentPath = folder.relativePath;
_currentPath = folder.path;
});
_loadFolders();
}
@@ -81,10 +80,7 @@ class _FolderPickerState extends State<FolderPicker> {
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: widget.maxVisibleItems != null
? (widget.maxVisibleItems! * 56.0 + 40.0).clamp(
80.0,
maxHeight,
)
? (widget.maxVisibleItems! * 56.0 + 40.0).clamp(80.0, maxHeight)
: maxHeight,
),
child: _buildListContent(context, primaryColor),
@@ -112,10 +108,7 @@ class _FolderPickerState extends State<FolderPicker> {
children: [
Icon(Icons.folder_off, size: 48, color: Colors.grey.shade400),
const SizedBox(height: 12),
Text(
'此文件夹为空',
style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
),
Text('此文件夹为空', style: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
],
),
);
@@ -153,13 +146,7 @@ class _FolderPickerState extends State<FolderPicker> {
),
const SizedBox(width: 12),
Expanded(
child: Text(
folder.name,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
child: Text(folder.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
),
Icon(Icons.chevron_right, color: Colors.grey.shade400, size: 20),
],
@@ -169,7 +156,7 @@ class _FolderPickerState extends State<FolderPicker> {
}
Widget _buildBreadcrumb(BuildContext context, Color primaryColor) {
final pathParts = FileUtils.toRelativePath(_currentPath).split('/');
final pathParts = _currentPath.split('/');
pathParts.removeWhere((part) => part.isEmpty);
return Column(
@@ -193,8 +180,8 @@ class _FolderPickerState extends State<FolderPicker> {
),
...pathParts.asMap().entries.expand((entry) {
final index = entry.key;
final path =
'/${pathParts.sublist(0, index + 1).join('/')}';
final part = entry.value;
final path = '/${pathParts.sublist(0, index + 1).join('/')}';
return [
Padding(
@@ -207,7 +194,7 @@ class _FolderPickerState extends State<FolderPicker> {
),
_buildBreadcrumbItem(
context,
name: FileUtils.decodePathSegment(entry.value),
name: part,
path: path,
isLast: index == pathParts.length - 1,
primaryColor: primaryColor,
@@ -221,14 +208,13 @@ class _FolderPickerState extends State<FolderPicker> {
const SizedBox(width: 12),
FilledButton.tonal(
onPressed: () {
final relative = FileUtils.toRelativePath(_currentPath);
final relative = _currentPath.startsWith('cloudreve://my')
? _currentPath.replaceFirst('cloudreve://my', '')
: _currentPath;
widget.onFolderSelected(relative.isEmpty ? '/' : relative);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('选择'),
),
+1 -1
View File
@@ -436,7 +436,7 @@ class _SearchDialogState extends State<SearchDialog> {
),
const SizedBox(height: 2),
Text(
FileUtils.decodePathForDisplay(file.relativePath),
FileUtils.safeDecodePathSegment(file.relativePath),
style: TextStyle(fontSize: 12, color: theme.hintColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
+143
View File
@@ -0,0 +1,143 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../../data/models/app_update_model.dart';
import '../../services/update_service.dart';
import 'toast_helper.dart';
class UpdatePrompt extends StatefulWidget {
final Widget child;
const UpdatePrompt({super.key, required this.child});
@override
State<UpdatePrompt> createState() => _UpdatePromptState();
}
class _UpdatePromptState extends State<UpdatePrompt> {
static bool _checkedThisRun = false;
bool _dialogVisible = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdate());
}
Future<void> _checkUpdate() async {
if (_checkedThisRun || _dialogVisible || !mounted) return;
_checkedThisRun = true;
final update = await UpdateService.instance.checkForUpdate();
if (update == null || !mounted) return;
_dialogVisible = true;
try {
await _showUpdateDialog(update);
} finally {
_dialogVisible = false;
}
}
Future<void> _showUpdateDialog(AppUpdateInfo update) async {
var downloading = false;
var progress = 0.0;
String? status;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
final canDownload =
update.downloadUrl != null &&
(Platform.isAndroid || Platform.isWindows);
Future<void> startUpdate() async {
if (downloading || !canDownload) return;
setDialogState(() {
downloading = true;
progress = 0;
status = '正在下载更新...';
});
try {
final file = await UpdateService.instance.downloadUpdate(
update,
onProgress: (received, total) {
if (!mounted || total <= 0) return;
setDialogState(() {
progress = received / total;
});
},
);
if (file == null) return;
setDialogState(() {
progress = 1;
status = '下载完成,正在打开安装包...';
});
await UpdateService.instance.openInstaller(file);
if (dialogContext.mounted) Navigator.of(dialogContext).pop();
} catch (e) {
setDialogState(() {
downloading = false;
status = '下载或打开安装包失败';
});
ToastHelper.failure('更新失败: $e');
}
}
return AlertDialog(
title: Text('发现新版本 ${update.version}'),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(update.title),
if ((update.description ?? '').isNotEmpty) ...[
const SizedBox(height: 12),
Text(
update.description!,
maxLines: 6,
overflow: TextOverflow.ellipsis,
),
],
if (downloading) ...[
const SizedBox(height: 16),
LinearProgressIndicator(
value: progress == 0 ? null : progress,
),
const SizedBox(height: 8),
Text(status ?? ''),
],
],
),
),
actions: [
TextButton(
onPressed: downloading
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('稍后'),
),
FilledButton(
onPressed: downloading ? null : startUpdate,
child: const Text('立即更新'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) => widget.child;
}