This commit is contained in:
@@ -27,4 +27,12 @@ class StorageKeys {
|
||||
|
||||
// 搜索历史
|
||||
static const String searchHistory = 'search_history';
|
||||
|
||||
// 同步相关
|
||||
static const String syncConfig = 'sync_config';
|
||||
static const String syncState = 'sync_state';
|
||||
static const String clientId = 'client_id';
|
||||
|
||||
// 日志级别
|
||||
static const String logLevel = 'app_log_level';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:io';
|
||||
|
||||
class SyncDefaults {
|
||||
SyncDefaults._();
|
||||
|
||||
/// 默认同步目录
|
||||
static String defaultLocalRoot() {
|
||||
if (Platform.isWindows) {
|
||||
return '${Platform.environment['USERPROFILE']}\\Documents\\Cloudreve4';
|
||||
} else if (Platform.isLinux) {
|
||||
final xdgDownload =
|
||||
Platform.environment['XDG_DOWNLOAD_DIR'] ?? ("${Platform.environment['HOME'] ?? ''}/Downloads");
|
||||
return '$xdgDownload/Cloudreve4';
|
||||
} else if (Platform.isAndroid) {
|
||||
return ''; // Android 使用系统相册目录
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static const String defaultRemoteRoot = 'cloudreve://my';
|
||||
static const String defaultSyncMode = 'full';
|
||||
static const String defaultConflictStrategy = 'keep_both';
|
||||
static const int defaultMaxConcurrentTransfers = 3;
|
||||
static const int defaultBandwidthLimitKbps = 0;
|
||||
static const int defaultMaxWorkers = 0; // 0 = CPU 核心数
|
||||
static const String defaultLogLevel = 'info';
|
||||
}
|
||||
@@ -10,6 +10,10 @@ class AppLogger {
|
||||
static Logger? _logger;
|
||||
static File? _logFile;
|
||||
|
||||
/// 当前日志级别(默认 info,debug 模式下也是 info 避免刷屏)
|
||||
static Level _level = Level.info;
|
||||
static Level get level => _level;
|
||||
|
||||
/// 初始化日志,必须在 main 中 await
|
||||
static Future<void> init() async {
|
||||
if (_logger != null) return;
|
||||
@@ -22,6 +26,10 @@ class AppLogger {
|
||||
}
|
||||
_logFile = File(p.join(logDir.path, 'log.txt'));
|
||||
|
||||
_createLogger();
|
||||
}
|
||||
|
||||
static void _createLogger() {
|
||||
// 2. 配置多路输出:同时输出到控制台和文件
|
||||
_logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
@@ -38,10 +46,16 @@ class AppLogger {
|
||||
file: _logFile!,
|
||||
),
|
||||
]),
|
||||
filter: ProductionFilter(),
|
||||
filter: _LevelFilter(_level),
|
||||
);
|
||||
}
|
||||
|
||||
/// 运行时切换日志级别
|
||||
static void setLevel(Level level) {
|
||||
_level = level;
|
||||
_createLogger();
|
||||
}
|
||||
|
||||
// 使用 getter 确保 logger 已初始化,防止空指针
|
||||
static Logger get _instance {
|
||||
_logger ??= Logger(
|
||||
@@ -67,6 +81,9 @@ class AppLogger {
|
||||
/// Error 级别日志
|
||||
static void e(String message) => _instance.e(message);
|
||||
|
||||
/// Trace 级别日志(高频轮询/查询使用,仅 trace 级别可见)
|
||||
static void t(String message) => _instance.t(message);
|
||||
|
||||
/// Debug 级别日志(支持格式化)
|
||||
static void df(String message, List<Object> args) => _instance.d(message, error: args);
|
||||
|
||||
@@ -132,6 +149,17 @@ class AppLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/// 自定义级别过滤器:低于设定级别的日志被过滤
|
||||
class _LevelFilter extends LogFilter {
|
||||
final Level minLevel;
|
||||
_LevelFilter(this.minLevel);
|
||||
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
return event.level.index >= minLevel.index;
|
||||
}
|
||||
}
|
||||
|
||||
/// 定义一个简单的自定义 FileOutput,防止 Logger 自带版本不支持追加
|
||||
class CustomFileOutput extends LogOutput {
|
||||
final File file;
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
/// 文件工具类
|
||||
class FileUtils {
|
||||
/// 将路径转换为 Cloudreve URI 格式
|
||||
/// "/" → "cloudreve://my", "/subfolder" → "cloudreve://my/subfolder"
|
||||
static String safeDecodePathSegment(String value, {int maxPasses = 5}) {
|
||||
var decoded = value;
|
||||
|
||||
for (var i = 0; i < maxPasses; i++) {
|
||||
try {
|
||||
final next = Uri.decodeComponent(decoded);
|
||||
if (next == decoded) break;
|
||||
decoded = next;
|
||||
} on FormatException {
|
||||
break;
|
||||
} on ArgumentError {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
static String decodePathForDisplay(String path) {
|
||||
return path
|
||||
.split('/')
|
||||
.map((segment) => safeDecodePathSegment(segment))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
static String toCloudreveUri(String path) {
|
||||
if (path.startsWith('cloudreve://')) return path;
|
||||
if (path == '/' || path.isEmpty) return 'cloudreve://my';
|
||||
@@ -11,69 +33,18 @@ class FileUtils {
|
||||
|
||||
static String toCloudreveUriWithQuery(
|
||||
String path,
|
||||
Map<String, Object?> queryParameters,
|
||||
Map<String, String> queryParameters,
|
||||
) {
|
||||
final baseUri = toCloudreveUri(path);
|
||||
final filteredQuery = <String, String>{};
|
||||
|
||||
for (final entry in queryParameters.entries) {
|
||||
final value = entry.value;
|
||||
if (value != null) {
|
||||
filteredQuery[entry.key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredQuery.isEmpty) return baseUri;
|
||||
|
||||
final query = Uri(queryParameters: filteredQuery).query;
|
||||
final separator = baseUri.contains('?') ? '&' : '?';
|
||||
return '$baseUri$separator$query';
|
||||
final uri = Uri.parse(toCloudreveUri(path));
|
||||
return uri.replace(queryParameters: queryParameters).toString();
|
||||
}
|
||||
|
||||
static String toRelativePath(String path) {
|
||||
const prefix = 'cloudreve://my';
|
||||
if (!path.startsWith(prefix)) {
|
||||
return path.isEmpty ? '/' : path;
|
||||
}
|
||||
|
||||
var relative = path.substring(prefix.length);
|
||||
final queryIndex = relative.indexOf('?');
|
||||
if (queryIndex != -1) {
|
||||
relative = relative.substring(0, queryIndex);
|
||||
}
|
||||
|
||||
return relative.isEmpty ? '/' : relative;
|
||||
}
|
||||
|
||||
static String decodePathForDisplay(String path) {
|
||||
final relativePath = toRelativePath(path);
|
||||
return relativePath.split('/').map(decodePathSegment).join('/');
|
||||
}
|
||||
|
||||
static String decodePathSegment(String value) {
|
||||
var decoded = value;
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
try {
|
||||
final next = Uri.decodeComponent(decoded);
|
||||
if (next == decoded) break;
|
||||
decoded = next;
|
||||
} on FormatException {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/// 获取文件扩展名
|
||||
static String getFileExtension(String fileName) {
|
||||
final dotIndex = fileName.lastIndexOf('.');
|
||||
if (dotIndex == -1) return '';
|
||||
return fileName.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/// 判断是否为图片文件
|
||||
static bool isImageFile(String fileName) {
|
||||
const imageExtensions = [
|
||||
'jpg',
|
||||
@@ -88,24 +59,20 @@ class FileUtils {
|
||||
return imageExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为视频文件
|
||||
static bool isVideoFile(String fileName) {
|
||||
const videoExtensions = ['mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'wmv'];
|
||||
return videoExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为音频文件
|
||||
static bool isAudioFile(String fileName) {
|
||||
const audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
|
||||
return audioExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为PDF文件
|
||||
static bool isPdfFile(String fileName) {
|
||||
return getFileExtension(fileName) == 'pdf';
|
||||
}
|
||||
|
||||
/// 判断是否为文本文件
|
||||
static bool isTextFile(String fileName) {
|
||||
const textExtensions = [
|
||||
'txt',
|
||||
@@ -120,7 +87,6 @@ class FileUtils {
|
||||
return textExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为代码文件
|
||||
static bool isCodeFile(String fileName) {
|
||||
const codeExtensions = [
|
||||
'js',
|
||||
@@ -152,13 +118,11 @@ class FileUtils {
|
||||
return codeExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为压缩文件
|
||||
static bool isArchiveFile(String fileName) {
|
||||
const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'];
|
||||
return archiveExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否为文档文件
|
||||
static bool isDocumentFile(String fileName) {
|
||||
const docExtensions = [
|
||||
'doc',
|
||||
@@ -174,7 +138,6 @@ class FileUtils {
|
||||
return docExtensions.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
/// 判断是否可预览
|
||||
static bool isPreviewable(String fileName) {
|
||||
return isImageFile(fileName) ||
|
||||
isVideoFile(fileName) ||
|
||||
@@ -183,7 +146,6 @@ class FileUtils {
|
||||
isCodeFile(fileName);
|
||||
}
|
||||
|
||||
/// 获取MIME类型
|
||||
static String getMimeType(String fileName) {
|
||||
final ext = getFileExtension(fileName);
|
||||
final mimeTypes = {
|
||||
@@ -211,7 +173,6 @@ class FileUtils {
|
||||
return mimeTypes[ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/// 获取文件图标
|
||||
static String getFileIcon(String fileName) {
|
||||
if (isImageFile(fileName)) return 'assets/icons/image.svg';
|
||||
if (isVideoFile(fileName)) return 'assets/icons/video.svg';
|
||||
|
||||
Reference in New Issue
Block a user