二次开发源码提交

This commit is contained in:
2026-05-15 08:52:48 +08:00
parent 4131f7321a
commit eb35a1d067
191 changed files with 34566 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
/// 应用常量
class AppConstants {
// 存储键
static const String keyAccessToken = 'access_token';
static const String keyRefreshToken = 'refresh_token';
static const String keyAccessExpires = 'access_expires';
static const String keyRefreshExpires = 'refresh_expires';
static const String keyRememberMe = 'remember_me';
static const String keyThemeMode = 'theme_mode';
static const String keyLanguage = 'language';
// 文件类型
static const int fileTypeFile = 0;
static const int fileTypeFolder = 1;
// 默认图标
static const String defaultAvatar = 'assets/images/default_avatar.png';
// 时间格式
static const String dateFormat = 'yyyy-MM-dd';
static const String dateTimeFormat = 'yyyy-MM-dd HH:mm:ss';
static const String timeFormat = 'HH:mm:ss';
}
/// API响应码
class ApiCode {
static const int success = 0;
static const int continueCode = 203;
static const int credentialInvalid = 40020;
static const int incorrectPassword = 40069;
static const int lockConflict = 40073;
static const int staleVersion = 40076;
static const int credentialRequired = 401;
static const int permissionDenied = 403;
static const int notFound = 404;
static const int internalError = 500;
}
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class QuickAccessConfig {
final String id;
final String label;
final IconData icon;
final String path;
final Color color;
final bool isDefault;
const QuickAccessConfig({
required this.id,
required this.label,
required this.icon,
required this.path,
required this.color,
this.isDefault = false,
});
QuickAccessConfig copyWith({String? label, String? path, IconData? icon, Color? color}) =>
QuickAccessConfig(
id: id,
label: label ?? this.label,
icon: icon ?? this.icon,
path: path ?? this.path,
color: color ?? this.color,
isDefault: isDefault,
);
static const storageKey = 'quick_access_shortcuts_v2';
static const defaults = [
QuickAccessConfig(id: 'img', label: '图片', icon: LucideIcons.image, path: '/Images', color: Color(0xFFF0ABFC), isDefault: true),
QuickAccessConfig(id: 'vid', label: '视频', icon: LucideIcons.video, path: '/Videos', color: Color(0xFFFCD34D), isDefault: true),
QuickAccessConfig(id: 'doc', label: '文档', icon: LucideIcons.fileText, path: '/Documents', color: Color(0xFF93C5FD), isDefault: true),
QuickAccessConfig(id: 'mus', label: '音乐', icon: LucideIcons.music, path: '/Music', color: Color(0xFF86EFAC), isDefault: true),
];
static const iconPool = <IconData>[
LucideIcons.image,
LucideIcons.video,
LucideIcons.fileText,
LucideIcons.music,
LucideIcons.folder,
LucideIcons.download,
LucideIcons.archive,
LucideIcons.code,
LucideIcons.bookOpen,
LucideIcons.camera,
LucideIcons.film,
LucideIcons.headphones,
];
static const colorPool = <Color>[
Color(0xFFF0ABFC),
Color(0xFFFCD34D),
Color(0xFF93C5FD),
Color(0xFF86EFAC),
Color(0xFFFCA5A5),
Color(0xFFFDBA74),
Color(0xFFC4B5FD),
Color(0xFF67E8F9),
];
Map<String, dynamic> toJson() => {
'id': id,
'label': label,
'iconCode': icon.codePoint,
'path': path,
'color': color.toARGB32(),
'isDefault': isDefault,
};
factory QuickAccessConfig.fromJson(Map<String, dynamic> json) {
final iconCode = json['iconCode'] as int? ?? LucideIcons.folder.codePoint;
final matchedIcon = iconPool.cast<IconData?>().firstWhere(
(i) => i!.codePoint == iconCode,
orElse: () => LucideIcons.folder,
)!;
return QuickAccessConfig(
id: json['id'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(),
label: json['label'] as String,
icon: matchedIcon,
path: json['path'] as String,
color: Color(json['color'] as int? ?? 0xFF93C5FD),
isDefault: json['isDefault'] as bool? ?? false,
);
}
static List<QuickAccessConfig> parseSaved(String saved) {
try {
final list = jsonDecode(saved) as List<dynamic>;
return list.map((e) => QuickAccessConfig.fromJson(e as Map<String, dynamic>)).toList();
} catch (_) {
return List.from(defaults);
}
}
static String serialize(List<QuickAccessConfig> items) {
return jsonEncode(items.map((e) => e.toJson()).toList());
}
/// 迁移旧格式(分号分隔的路径列表)
static List<QuickAccessConfig> migrateV1(String saved) {
final parts = saved.split(';');
final items = <QuickAccessConfig>[];
for (int i = 0; i < defaults.length; i++) {
if (i < parts.length && parts[i].isNotEmpty) {
items.add(defaults[i].copyWith(path: parts[i]));
} else {
items.add(defaults[i]);
}
}
return items;
}
}
extension ColorDarken on Color {
Color darken(double amount) {
final hsl = HSLColor.fromColor(this);
return hsl
.withLightness((hsl.lightness - amount).clamp(0.0, 1.0))
.toColor();
}
}
+30
View File
@@ -0,0 +1,30 @@
/// 存储键常量
class StorageKeys {
// 设置相关
static const String themeMode = 'theme_mode';
static const String customBaseUrl = 'custom_base_url';
static const String servers = 'flutter_servers';
static const String lastSelectedServer = 'last_selected_server_label';
// 上传相关
static const String uploadQueue = 'upload_queue';
static const String uploadTasks = 'upload_tasks';
// 下载相关
static const String downloadTasks = 'download_tasks';
static const String downloadWifiOnly = 'download_wifi_only';
static const String downloadRetries = 'download_retries';
// 任务记录
static const String taskRetentionDays = 'task_retention_days';
// 缓存相关
static const String cacheSettings = 'cache_settings';
// Gravatar 镜像
static const String gravatarMirrorEnabled = 'gravatar_mirror_enabled';
static const String gravatarMirrorUrl = 'gravatar_mirror_url';
// 搜索历史
static const String searchHistory = 'search_history';
}
+61
View File
@@ -0,0 +1,61 @@
/// 应用异常基类
class AppException implements Exception {
final String message;
final int? code;
final dynamic data;
AppException(this.message, {this.code, this.data});
@override
String toString() => 'AppException: $message (code: $code)';
}
/// 网络异常
class NetworkException extends AppException {
NetworkException(super.message, {super.code, super.data});
}
/// 认证异常
class AuthException extends AppException {
AuthException(super.message, {super.code, super.data});
}
/// 服务器异常
class ServerException extends AppException {
ServerException(super.message, {super.code, super.data});
}
/// Token过期异常
class TokenExpiredException extends AuthException {
TokenExpiredException()
: super('Token已过期,请重新登录', code: 401);
}
/// RefreshToken过期异常
class RefreshTokenExpiredException extends AuthException {
RefreshTokenExpiredException()
: super('登录已过期,请重新登录', code: 40020);
}
/// 未授权异常
class UnauthorizedException extends AuthException {
UnauthorizedException() : super('未授权,请先登录', code: 403);
}
/// 文件不存在异常
class FileNotFoundException extends AppException {
FileNotFoundException() : super('文件不存在', code: 404);
}
/// 存储空间不足异常
class StorageSpaceException extends AppException {
StorageSpaceException() : super('存储空间不足', code: 507);
}
/// 两步验证需要异常
class TwoFactorRequiredException extends AuthException {
final String sessionId;
TwoFactorRequiredException(this.sessionId)
: super('需要两步验证', code: 203);
}
+161
View File
@@ -0,0 +1,161 @@
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
/// 应用日志类
class AppLogger {
AppLogger._();
static Logger? _logger;
static File? _logFile;
/// 初始化日志,必须在 main 中 await
static Future<void> init() async {
if (_logger != null) return;
// 1. 获取日志存储路径 (Windows: $HOME/AppData/Roaming/com.limo/cloudreve4_flutter/logs)
final appDir = await getApplicationSupportDirectory();
final logDir = Directory(p.join(appDir.path, 'logs'));
if (!await logDir.exists()) {
await logDir.create(recursive: true);
}
_logFile = File(p.join(logDir.path, 'log.txt'));
// 2. 配置多路输出:同时输出到控制台和文件
_logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
errorMethodCount: 5,
lineLength: 80,
colors: true,
printEmojis: true,
dateTimeFormat: DateTimeFormat.dateAndTime,
),
output: MultiOutput([
ConsoleOutput(),
CustomFileOutput(
file: _logFile!,
),
]),
filter: ProductionFilter(),
);
}
// 使用 getter 确保 logger 已初始化,防止空指针
static Logger get _instance {
_logger ??= Logger(
printer: PrettyPrinter(
methodCount: 0,
colors: true,
printEmojis: true,
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
),
);
return _logger!;
}
/// Debug 级别日志
static void d(String message) => _instance.d(message);
/// Info 级别日志
static void i(String message) => _instance.i(message);
/// Warning 级别日志
static void w(String message) => _instance.w(message);
/// Error 级别日志
static void e(String message) => _instance.e(message);
/// Debug 级别日志(支持格式化)
static void df(String message, List<Object> args) => _instance.d(message, error: args);
/// Info 级别日志(支持格式化)
static void ifn(String message, List<Object> args) => _instance.i(message, error: args);
/// Warning 级别日志(支持格式化)
static void wf(String message, List<Object> args) => _instance.w(message, error: args);
/// Error 级别日志(支持格式化)
static void ef(String message, List<Object> args) => _instance.e(message, error: args);
/// 获取日志文件路径
static Future<String> get logFilePath async {
if (_logFile != null) return _logFile!.path;
final appDir = await getApplicationSupportDirectory();
return p.join(appDir.path, 'logs', 'log.txt');
}
/// 获取日志文件大小(字节)
static Future<int> get logFileSize async {
final path = await logFilePath;
final file = File(path);
if (await file.exists()) {
return await file.length();
}
return 0;
}
/// 清空日志文件(内容清零,不删除文件)
static Future<void> clearLog() async {
final path = await logFilePath;
final file = File(path);
if (await file.exists()) {
await file.writeAsString('');
}
}
/// 导出日志文件到指定目录
static Future<String?> exportLog(String targetDir) async {
final srcPath = await logFilePath;
final srcFile = File(srcPath);
if (!await srcFile.exists()) return null;
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').substring(0, 19);
final destPath = p.join(targetDir, 'cloudreve_log_$timestamp.txt');
await srcFile.copy(destPath);
return destPath;
}
/// 读取日志内容(用于预览)
static Future<String> readLog({int maxLines = 500}) async {
final path = await logFilePath;
final file = File(path);
if (!await file.exists()) return '';
final lines = await file.readAsLines();
if (lines.length <= maxLines) {
return lines.join('\n');
}
return '... (仅显示最近 $maxLines 行)\n\n'
'${lines.sublist(lines.length - maxLines).join('\n')}';
}
}
/// 定义一个简单的自定义 FileOutput,防止 Logger 自带版本不支持追加
class CustomFileOutput extends LogOutput {
final File file;
CustomFileOutput({required this.file});
@override
void output(OutputEvent event) {
for (var line in event.lines) {
// 过滤掉 ANSI 颜色代码,防止 log.txt 乱码
final cleanLine = line.replaceAll(RegExp(r'\x1B\[[0-9;]*[a-zA-Z]'), '');
file.writeAsStringSync('$cleanLine\n', mode: FileMode.writeOnlyAppend);
}
}
}
/// 日志帮助类
/// 提供一个全局的静态日志实例
class Log {
static void d(String message) => AppLogger.d(message);
static void i(String message) => AppLogger.i(message);
static void w(String message) => AppLogger.w(message);
static void e(String message) => AppLogger.e(message);
static void df(String message, List<Object> args) => AppLogger.df(message, args);
static void ifn(String message, List<Object> args) => AppLogger.ifn(message, args);
static void wf(String message, List<Object> args) => AppLogger.wf(message, args);
static void ef(String message, List<Object> args) => AppLogger.ef(message, args);
}
+46
View File
@@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:cloudreve4_flutter/services/storage_service.dart';
import 'package:cloudreve4_flutter/core/constants/storage_keys.dart';
/// 头像相关工具
class AvatarUtils {
AvatarUtils._();
/// 获取 Gravatar URL(异步,因为需要读取镜像配置)
static Future<String> getGravatarUrl(String email, {int size = 200}) async {
final cleanEmail = email.trim().toLowerCase();
final hash = md5.convert(utf8.encode(cleanEmail)).toString();
final mirror = await getGravatarMirror();
final base = mirror ?? 'https://www.gravatar.com';
return '$base/avatar/$hash?s=$size&d=identicon';
}
/// 获取服务器头像 URL
static String getServerAvatarUrl(String baseUrl, String userId) {
return '$baseUrl/user/avatar/$userId';
}
/// 获取 Gravatar 镜像配置
static Future<String?> getGravatarMirror() async {
final enabled = await StorageService.instance
.getBool(StorageKeys.gravatarMirrorEnabled) ??
true;
if (!enabled) return null;
return await StorageService.instance
.getString(StorageKeys.gravatarMirrorUrl) ??
'https://weavatar.com';
}
/// 设置 Gravatar 镜像启用状态
static Future<void> setGravatarMirrorEnabled(bool enabled) async {
await StorageService.instance
.setBool(StorageKeys.gravatarMirrorEnabled, enabled);
}
/// 设置 Gravatar 镜像地址
static Future<void> setGravatarMirrorUrl(String? url) async {
await StorageService.instance
.setString(StorageKeys.gravatarMirrorUrl, url);
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'package:intl/intl.dart';
/// 日期工具类
class DateUtils {
/// 格式化日期
static String formatDate(DateTime? date, {String pattern = 'yyyy-MM-dd'}) {
if (date == null) return '-';
final localDate = date.isUtc ? date.toLocal() : date;
return DateFormat(pattern).format(localDate);
}
/// 格式化日期时间
static String formatDateTime(DateTime? dateTime) {
return formatDate(dateTime, pattern: 'yyyy-MM-dd HH:mm:ss');
}
/// 格式化时间
static String formatTime(DateTime? dateTime) {
return formatDate(dateTime, pattern: 'HH:mm:ss');
}
/// 格式化文件大小
static String formatFileSize(int? bytes) {
if (bytes == null || bytes < 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
double size = bytes.toDouble();
int unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return '${size.toStringAsFixed(2)} ${units[unitIndex]}';
}
/// 获取相对时间描述
static String getRelativeTime(DateTime? dateTime) {
if (dateTime == null) return '-';
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inSeconds < 60) {
return '${difference.inSeconds}秒前';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes}分钟前';
} else if (difference.inHours < 24) {
return '${difference.inHours}小时前';
} else if (difference.inDays < 30) {
return '${difference.inDays}天前';
} else if (difference.inDays < 365) {
return '${(difference.inDays / 30).floor()}个月前';
} else {
return '${(difference.inDays / 365).floor()}年前';
}
}
/// 判断是否为今天
static bool isToday(DateTime? dateTime) {
if (dateTime == null) return false;
final now = DateTime.now();
return dateTime.year == now.year &&
dateTime.month == now.month &&
dateTime.day == now.day;
}
/// 判断是否为昨天
static bool isYesterday(DateTime? dateTime) {
if (dateTime == null) return false;
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
return dateTime.year == yesterday.year &&
dateTime.month == yesterday.month &&
dateTime.day == yesterday.day;
}
}
+232
View File
@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../data/models/file_model.dart';
import 'file_utils.dart';
/// 文件图标、颜色、类型标签统一管理
class FileIconUtils {
// ---- 文件图标 ----
// 后缀分类映射:安装包/可执行/字体/数据库/镜像等
static const _installerExtensions = {'apk', 'exe', 'msi', 'dmg', 'deb', 'rpm', 'appimage', 'snap'};
static const _executableExtensions = {'sh', 'bat', 'cmd', 'ps1', 'bin', 'run'};
static const _fontExtensions = {'ttf', 'otf', 'woff', 'woff2', 'eot'};
static const _databaseExtensions = {'db', 'sqlite', 'sqlite3', 'mdb', 'sql'};
static const _imageDiskExtensions = {'iso', 'img', 'vmdk', 'vdi', 'qcow2'};
static const _torrentExtensions = {'torrent'};
static IconData getFileIcon(String fileName) {
if (FileUtils.isImageFile(fileName)) return LucideIcons.image;
if (FileUtils.isVideoFile(fileName)) return LucideIcons.video;
if (FileUtils.isAudioFile(fileName)) return LucideIcons.music;
if (FileUtils.isPdfFile(fileName)) return LucideIcons.fileText;
if (FileUtils.isTextFile(fileName)) return LucideIcons.fileText;
if (FileUtils.isCodeFile(fileName)) return LucideIcons.code;
if (FileUtils.isArchiveFile(fileName)) return LucideIcons.archive;
if (FileUtils.isDocumentFile(fileName)) return LucideIcons.file;
// fallback: 按后缀分类
final ext = FileUtils.getFileExtension(fileName);
if (_installerExtensions.contains(ext)) return LucideIcons.package;
if (_executableExtensions.contains(ext)) return LucideIcons.terminal;
if (_fontExtensions.contains(ext)) return LucideIcons.type;
if (_databaseExtensions.contains(ext)) return LucideIcons.database;
if (_imageDiskExtensions.contains(ext)) return LucideIcons.hardDrive;
if (_torrentExtensions.contains(ext)) return LucideIcons.download;
return LucideIcons.file;
}
static Color getFileIconColor(String fileName) {
if (FileUtils.isImageFile(fileName)) return const Color(0xFFA855F7);
if (FileUtils.isVideoFile(fileName)) return const Color(0xFFF97316);
if (FileUtils.isAudioFile(fileName)) return const Color(0xFF3B82F6);
if (FileUtils.isPdfFile(fileName)) return const Color(0xFFEF4444);
if (FileUtils.isTextFile(fileName)) return const Color(0xFF14B8A6);
if (FileUtils.isCodeFile(fileName)) return const Color(0xFF06B6D4);
if (FileUtils.isArchiveFile(fileName)) return const Color(0xFFF59E0B);
if (FileUtils.isDocumentFile(fileName)) return const Color(0xFF6366F1);
// fallback: 按后缀分类
final ext = FileUtils.getFileExtension(fileName);
if (_installerExtensions.contains(ext)) return const Color(0xFF22C55E);
if (_executableExtensions.contains(ext)) return const Color(0xFF10B981);
if (_fontExtensions.contains(ext)) return const Color(0xFF8B5CF6);
if (_databaseExtensions.contains(ext)) return const Color(0xFF6366F1);
if (_imageDiskExtensions.contains(ext)) return const Color(0xFF64748B);
if (_torrentExtensions.contains(ext)) return const Color(0xFF06B6D4);
// 真正未知:基于后缀名 hash 生成稳定颜色
if (ext.isNotEmpty) return _stableColor(ext);
return const Color(0xFF64748B);
}
/// 基于字符串生成稳定的颜色(避免同类型不同后缀撞色)
static Color _stableColor(String key) {
final hash = key.hashCode.abs();
const palette = [
Color(0xFF64748B), Color(0xFF6366F1), Color(0xFF8B5CF6),
Color(0xFFEC4899), Color(0xFF14B8A6), Color(0xFFF59E0B),
Color(0xFF22C55E), Color(0xFF0EA5E9), Color(0xFFEF4444),
];
return palette[hash % palette.length];
}
/// 文件类型中文标签
static String getFileTypeLabel(String fileName, {bool isFolder = false}) {
if (isFolder) return '文件夹';
if (FileUtils.isImageFile(fileName)) return '图片';
if (FileUtils.isVideoFile(fileName)) return '视频';
if (FileUtils.isAudioFile(fileName)) return '音频';
if (FileUtils.isPdfFile(fileName)) return 'PDF';
if (FileUtils.isTextFile(fileName)) {
final ext = FileUtils.getFileExtension(fileName);
return ext.isNotEmpty ? '${ext.toUpperCase()}文件' : '文本';
}
if (FileUtils.isCodeFile(fileName)) {
final ext = FileUtils.getFileExtension(fileName);
return ext.isNotEmpty ? '${ext.toUpperCase()}文件' : '代码';
}
if (FileUtils.isArchiveFile(fileName)) return '压缩包';
if (FileUtils.isDocumentFile(fileName)) return '文档';
// fallback: 未知后缀直接用大写后缀名
final ext = FileUtils.getFileExtension(fileName);
if (ext.isNotEmpty) return '${ext.toUpperCase()}文件';
return '文件';
}
// ---- 语义化文件夹图标 ----
static final _folderIconMap = <String, IconData>{
'DOCUMENTS': LucideIcons.fileText,
'DOCS': LucideIcons.fileText,
'PICTURES': LucideIcons.image,
'PHOTOS': LucideIcons.image,
'IMAGES': LucideIcons.image,
'VIDEOS': LucideIcons.video,
'MOVIES': LucideIcons.video,
'MUSIC': LucideIcons.music,
'AUDIO': LucideIcons.music,
'DOWNLOADS': LucideIcons.download,
'GAMES': LucideIcons.gamepad2,
'OS_IMAGE': LucideIcons.hardDrive,
'DOCKER': LucideIcons.container,
'CONTAINERS': LucideIcons.container,
'ARIA2': LucideIcons.download,
'BACKUP': LucideIcons.archive,
'DESKTOP': LucideIcons.monitor,
'FAVORITES': LucideIcons.star,
'SHARE': LucideIcons.share2,
'SHARED': LucideIcons.share2,
'TRASH': LucideIcons.trash2,
'RECYCLE': LucideIcons.trash2,
};
static final _folderColorMap = <String, Color>{
'DOCUMENTS': const Color(0xFF3B82F6),
'DOCS': const Color(0xFF3B82F6),
'PICTURES': const Color(0xFFA855F7),
'PHOTOS': const Color(0xFFA855F7),
'IMAGES': const Color(0xFFA855F7),
'VIDEOS': const Color(0xFFF97316),
'MOVIES': const Color(0xFFF97316),
'MUSIC': const Color(0xFF3B82F6),
'AUDIO': const Color(0xFF3B82F6),
'DOWNLOADS': const Color(0xFF22C55E),
'GAMES': const Color(0xFF8B5CF6),
'OS_IMAGE': const Color(0xFF64748B),
'DOCKER': const Color(0xFF0EA5E9),
'CONTAINERS': const Color(0xFF0EA5E9),
'ARIA2': const Color(0xFF06B6D4),
'BACKUP': const Color(0xFFF59E0B),
'DESKTOP': const Color(0xFF6366F1),
'FAVORITES': const Color(0xFFEAB308),
'SHARE': const Color(0xFF14B8A6),
'SHARED': const Color(0xFF14B8A6),
'TRASH': const Color(0xFFEF4444),
'RECYCLE': const Color(0xFFEF4444),
};
static final _folderKeywords = <String, IconData>{
'文档': LucideIcons.fileText,
'图片': LucideIcons.image,
'相册': LucideIcons.image,
'视频': LucideIcons.video,
'音乐': LucideIcons.music,
'下载': LucideIcons.download,
'游戏': LucideIcons.gamepad2,
'备份': LucideIcons.archive,
'桌面': LucideIcons.monitor,
'收藏': LucideIcons.star,
'分享': LucideIcons.share2,
'回收站': LucideIcons.trash2,
};
static final _folderKeywordColors = <String, Color>{
'文档': const Color(0xFF3B82F6),
'图片': const Color(0xFFA855F7),
'相册': const Color(0xFFA855F7),
'视频': const Color(0xFFF97316),
'音乐': const Color(0xFF3B82F6),
'下载': const Color(0xFF22C55E),
'游戏': const Color(0xFF8B5CF6),
'备份': const Color(0xFFF59E0B),
'桌面': const Color(0xFF6366F1),
'收藏': const Color(0xFFEAB308),
'分享': const Color(0xFF14B8A6),
'回收站': const Color(0xFFEF4444),
};
static IconData getFolderIcon(String folderName) {
final upper = folderName.toUpperCase();
if (_folderIconMap.containsKey(upper)) return _folderIconMap[upper]!;
for (final entry in _folderKeywords.entries) {
if (folderName.contains(entry.key)) return entry.value;
}
return LucideIcons.folder;
}
static Color? getFolderIconColor(String folderName) {
final upper = folderName.toUpperCase();
if (_folderColorMap.containsKey(upper)) return _folderColorMap[upper]!;
for (final entry in _folderKeywordColors.entries) {
if (folderName.contains(entry.key)) return entry.value;
}
return null; // 使用 colorScheme.primary 作为默认
}
// ---- 统一图标容器组件 ----
static Widget buildIconWidget({
required BuildContext context,
required FileModel file,
double size = 36,
double iconSize = 20,
double borderRadius = 8,
}) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
if (file.isFolder) {
final folderColor = getFolderIconColor(file.name) ?? colorScheme.primary;
final folderIcon = getFolderIcon(file.name);
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: folderColor.withValues(alpha: isDark ? 0.15 : 0.1),
borderRadius: BorderRadius.circular(borderRadius),
),
child: Icon(folderIcon, color: folderColor, size: iconSize),
);
}
final icon = getFileIcon(file.name);
final iconColor = getFileIconColor(file.name);
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: iconColor.withValues(alpha: isDark ? 0.15 : 0.1),
borderRadius: BorderRadius.circular(borderRadius),
),
child: Icon(icon, color: iconColor, size: iconSize),
);
}
}
+171
View File
@@ -0,0 +1,171 @@
/// 文件类型工具类
class FileTypeUtils {
/// 图片扩展名
static const _imageExtensions = {
'jpg',
'jpeg',
'png',
'gif',
'webp',
'bmp',
'svg',
'ico',
'tiff',
'tif',
};
/// PDF扩展名
static const _pdfExtensions = {'pdf'};
/// 视频扩展名
static const _videoExtensions = {
'mp4',
'avi',
'mov',
'wmv',
'flv',
'mkv',
'webm',
'm4v',
'3gp',
'3g2',
};
/// 音频扩展名
static const _audioExtensions = {
'mp3',
'wav',
'ogg',
'flac',
'aac',
'm4a',
'wma',
};
/// 文档扩展名
static const _documentExtensions = {
'txt',
'md',
'json',
'xml',
'yaml',
'yml',
'ini',
'cfg',
'conf',
'log',
'csv',
};
/// 代码文件扩展名
static const _codeExtensions = {
'dart',
'js',
'ts',
'html',
'css',
'scss',
'less',
'python',
'py',
'java',
'kt',
'kts',
'swift',
'cpp',
'c',
'h',
'hpp',
'cs',
'go',
'rs',
'php',
'rb',
'sql',
'sh',
'bash',
'zsh',
'ps1',
'bat',
'cmd',
};
/// 检测是否为图片
static bool isImage(String fileName) {
final ext = getExtension(fileName);
return _imageExtensions.contains(ext);
}
/// 检测是否为PDF
static bool isPdf(String fileName) {
final ext = getExtension(fileName);
return _pdfExtensions.contains(ext);
}
/// 检测是否为视频
static bool isVideo(String fileName) {
final ext = getExtension(fileName);
return _videoExtensions.contains(ext);
}
/// 检测是否为音频
static bool isAudio(String fileName) {
final ext = getExtension(fileName);
return _audioExtensions.contains(ext);
}
/// 检测是否为文档
static bool isDocument(String fileName) {
final ext = getExtension(fileName);
return _documentExtensions.contains(ext) || _codeExtensions.contains(ext);
}
/// 检测是否为Markdown
static bool isMarkdown(String fileName) {
final ext = getExtension(fileName);
return ext == 'md';
}
/// 检测是否为文本或代码文件(排除markdown)
static bool isTextCode(String fileName) {
final ext = getExtension(fileName);
return (_documentExtensions.contains(ext) && ext != 'md') ||
_codeExtensions.contains(ext);
}
/// 检测是否支持预览
static bool isPreviewable(String fileName) {
return isImage(fileName) ||
isPdf(fileName) ||
isVideo(fileName) ||
isAudio(fileName) ||
isDocument(fileName);
}
/// 获取文件扩展名
static String getExtension(String fileName) {
final parts = fileName.split('.');
if (parts.length < 2) {
return '';
}
return parts.last.toLowerCase();
}
/// 获取文件类型描述
static String getFileTypeDescription(String fileName) {
if (isImage(fileName)) {
return '图片';
} else if (isPdf(fileName)) {
return 'PDF文档';
} else if (isVideo(fileName)) {
return '视频';
} else if (isAudio(fileName)) {
return '音频';
} else if (isMarkdown(fileName)) {
return 'Markdown文档';
} else if (isDocument(fileName)) {
return '文档';
}
return '文件';
}
}
+128
View File
@@ -0,0 +1,128 @@
/// 文件工具类
class FileUtils {
/// 将路径转换为 Cloudreve URI 格式
/// "/" → "cloudreve://my", "/subfolder" → "cloudreve://my/subfolder"
static String toCloudreveUri(String path) {
if (path.startsWith('cloudreve://')) return path;
if (path == '/' || path.isEmpty) return 'cloudreve://my';
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
return 'cloudreve://my/$cleanPath';
}
/// 获取文件扩展名
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', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'heic'
];
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', 'md', 'json', 'xml', 'yaml', 'yml', 'ini', 'conf'
];
return textExtensions.contains(getFileExtension(fileName));
}
/// 判断是否为代码文件
static bool isCodeFile(String fileName) {
const codeExtensions = [
'js', 'ts', 'tsx', 'jsx', 'dart', 'java', 'py', 'c', 'cpp',
'h', 'hpp', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt',
'html', 'css', 'scss', 'less', 'sql', 'sh', 'bat'
];
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', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'
];
return docExtensions.contains(getFileExtension(fileName));
}
/// 判断是否可预览
static bool isPreviewable(String fileName) {
return isImageFile(fileName) || isVideoFile(fileName) ||
isPdfFile(fileName) || isTextFile(fileName) || isCodeFile(fileName);
}
/// 获取MIME类型
static String getMimeType(String fileName) {
final ext = getFileExtension(fileName);
final mimeTypes = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'mp4': 'video/mp4',
'webm': 'video/webm',
'mkv': 'video/x-matroska',
'avi': 'video/x-msvideo',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'ogg': 'audio/ogg',
'pdf': 'application/pdf',
'txt': 'text/plain',
'json': 'application/json',
'xml': 'application/xml',
'html': 'text/html',
'css': 'text/css',
'js': 'application/javascript',
};
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';
if (isAudioFile(fileName)) return 'assets/icons/audio.svg';
if (isPdfFile(fileName)) return 'assets/icons/pdf.svg';
if (isTextFile(fileName)) return 'assets/icons/text.svg';
if (isCodeFile(fileName)) return 'assets/icons/code.svg';
if (isArchiveFile(fileName)) return 'assets/icons/archive.svg';
if (isDocumentFile(fileName)) return 'assets/icons/document.svg';
return 'assets/icons/file.svg';
}
}
+221
View File
@@ -0,0 +1,221 @@
class LanguagePreview {
/// 语言名称映射
static final getExtNameMapping = {
'1c': '1C',
'abnf': 'ABNF',
'accesslog': 'Access Log',
'actionscript': 'ActionScript',
'ada': 'Ada',
'angelscript': 'AngelScript',
'apache': 'Apache',
'applescript': 'AppleScript',
'arcade': 'Arcade',
'arduino': 'Arduino',
'armasm': 'ARM Assembly',
'asciidoc': 'AsciiDoc',
'aspectj': 'AspectJ',
'autohotkey': 'AutoHotkey',
'autoit': 'AutoIt',
'avrasm': 'AVR Assembly',
'awk': 'AWK',
'axapta': 'Axapta',
'bash': 'Bash',
'basic': 'BASIC',
'bnf': 'BNF',
'brainfuck': 'Brainfuck',
'cal': 'Cal',
'capnproto': 'Cap\'n Proto',
'ceylon': 'Ceylon',
'clean': 'Clean',
'clojure-repl': 'Clojure REPL',
'clojure': 'Clojure',
'cmake': 'CMake',
'coffeescript': 'CoffeeScript',
'coq': 'Coq',
'cos': 'Cos',
'cpp': 'C++',
'crmsh': 'crmsh',
'crystal': 'Crystal',
'c': 'C',
'cs': 'C#',
'csp': 'CSP',
'css': 'CSS',
'd': 'D',
'dart': 'Dart',
'delphi': 'Delphi',
'diff': 'Diff',
'django': 'Django',
'dns': 'DNS',
'dockerfile': 'Dockerfile',
'dos': 'DOS',
'dsconfig': 'DSConfig',
'dts': 'TypeScript',
'dust': 'Dust',
'ebnf': 'EBNF',
'elixir': 'Elixir',
'elm': 'Elm',
'erb': 'ERB',
'erlang-repl': 'Erlang REPL',
'erlang': 'Erlang',
'excel': 'Excel',
'fix': 'Fix',
'flix': 'Flix',
'fortran': 'Fortran',
'fsharp': 'F#',
'gams': 'GAMS',
'gauss': 'GAUSS',
'gcode': 'G-code',
'gherkin': 'Gherkin',
'glsl': 'GLSL',
'gml': 'GML',
'go': 'Go',
'golo': 'Golo',
'gradle': 'Gradle',
'groovy': 'Groovy',
'haml': 'HAML',
'handlebars': 'Handlebars',
'haskell': 'Haskell',
'haxe': 'Haxe',
'hsp': 'HSP',
'htmlbars': 'HTMLBars',
'http': 'HTTP',
'hy': 'Hy',
'inform7': 'Inform 7',
'ini': 'INI',
'irpf90': 'IRPF90',
'isbl': 'ISBL',
'java': 'Java',
'javascript': 'JavaScript',
'jboss-cli': 'JBoss CLI',
'json': 'JSON',
'julia-repl': 'Julia REPL',
'julia': 'Julia',
'kotlin': 'Kotlin',
'lasso': 'Lasso',
'ldif': 'LDIF',
'leaf': 'Leaf',
'less': 'Less',
'lisp': 'Lisp',
'livecodeserver': 'LiveCode Server',
'livescript': 'LiveScript',
'llvm': 'LLVM IR',
'lsl': 'LSL',
'lua': 'Lua',
'makefile': 'Makefile',
'markdown': 'Markdown',
'mathematica': 'Mathematica',
'matlab': 'MATLAB',
'maxima': 'Maxima',
'mel': 'MEL',
'mercury': 'Mercury',
'mipsasm': 'MIPS Assembly',
'mizar': 'Mizar',
'mojolicious': 'Mojolicious',
'monkey': 'Monkey',
'moonscript': 'MoonScript',
'n1ql': 'N1QL',
'nginx': 'Nginx',
'nimrod': 'Nimrod',
'nix': 'Nix',
'nsis': 'NSIS',
'objectivec': 'Objective-C',
'ocaml': 'OCaml',
'openscad': 'OpenSCAD',
'oxygene': 'Oxygene',
'parser3': 'Parser3',
'perl': 'Perl',
'pf': 'PF',
'pgsql': 'PostgreSQL',
'php': 'PHP',
'plaintext': 'Plain Text',
'pony': 'Pony',
'powershell': 'PowerShell',
'processing': 'Processing',
'profile': 'Profile',
'prolog': 'Prolog',
'properties': 'Properties',
'protobuf': 'Protocol Buffers',
'puppet': 'Puppet',
'purebasic': 'PureBASIC',
'python': 'Python',
'q': 'Q',
'qml': 'QML',
'r': 'R',
'reasonml': 'ReasonML',
'rib': 'RenderMan RIB',
'roboconf': 'Roboconf',
'routeros': 'RouterOS',
'rsl': 'RSL',
'ruby': 'Ruby',
'ruleslanguage': 'Rules Language',
'rust': 'Rust',
'sas': 'SAS',
'scala': 'Scala',
'scheme': 'Scheme',
'scilab': 'Scilab',
'scss': 'SCSS',
'shell': 'Shell',
'smali': 'Smali',
'smalltalk': 'Smalltalk',
'sml': 'SML',
'sqf': 'SQF',
'sql': 'SQL',
'stan': 'Stan',
'stata': 'Stata',
'step21': 'STEP 21',
'stylus': 'Stylus',
'subunit': 'SubUnit',
'swift': 'Swift',
'taggerscript': 'TaggerScript',
'tap': 'TAP',
'tcl': 'Tcl',
'tex': 'TeX',
'thrift': 'Thrift',
'tp': 'TP',
'twig': 'Twig',
'typescript': 'TypeScript',
'vala': 'Vala',
'vbnet': 'VB.NET',
'vbscript-html': 'VBScript HTML',
'vbscript': 'VBScript',
'verilog': 'Verilog',
'vhdl': 'VHDL',
'vim': 'Vim',
'x86asm': 'x86 Assembly',
'xl': 'XL',
'xml': 'XML',
'xquery': 'XQuery',
'yaml': 'YAML',
'zephir': 'Zephir',
'vue': 'Vue',
'graphql': 'GraphQL',
'gn': 'Gn',
'solidity': 'Solidity',
'txt': 'Plain Text',
'log': 'Log',
'cfg': 'Config',
'conf': 'Config',
'csv': 'CSV',
'h': 'C/C++ Header',
'hpp': 'C++ Header',
'cc': 'C++',
'cxx': 'C++',
'js': 'JavaScript',
'jsx': 'JavaScript JSX',
'ts': 'TypeScript',
'tsx': 'TypeScript JSX',
'py': 'Python',
'rs': 'Rust',
'rb': 'Ruby',
'kt': 'Kotlin',
'kts': 'Kotlin Script',
'html': 'HTML',
'htm': 'HTML',
'yml': 'YAML',
'sh': 'Bash',
'zsh': 'Zsh',
'crt': 'Plain Text',
'pem': 'Plain Text',
};
}
+86
View File
@@ -0,0 +1,86 @@
/// Cloudreve obfuscated thumbnail URL decoder.
/// Ported from the Go reference implementation in the API documentation.
class TimeFlowDecoder {
/// Decode an obfuscated time-flow string.
/// Tries current time, then ±1000ms to account for clock drift.
/// Returns null if all attempts fail.
static String? decodeTimeFlowString(String str) {
if (str.isEmpty) return null;
final timeNow = DateTime.now().millisecondsSinceEpoch;
for (final offset in [0, -1000, 1000]) {
final result = _decodeTimeFlowStringTime(str, timeNow + offset);
if (result != null) return result;
}
return null;
}
static String? _decodeTimeFlowStringTime(String str, int timeNowMillis) {
if (str.isEmpty) return null;
int timeNow = timeNowMillis ~/ 1000;
final timeNowBackup = timeNow;
final timeDigits = <int>[];
if (timeNow == 0) {
timeDigits.add(0);
} else {
int tempTime = timeNow;
while (tempTime > 0) {
timeDigits.add(tempTime % 10);
tempTime = tempTime ~/ 10;
}
}
final res = str.split('');
var secret = str.split('');
var add = secret.length % 2 == 0;
var timeDigitIndex = (secret.length - 1) % timeDigits.length;
final l = secret.length;
for (int pos = 0; pos < l; pos++) {
final targetIndex = l - 1 - pos;
int newIndex = targetIndex;
if (add) {
newIndex += timeDigits[timeDigitIndex] * timeDigitIndex;
} else {
newIndex = 2 * timeDigitIndex * timeDigits[timeDigitIndex] - newIndex;
}
if (newIndex < 0) {
newIndex = newIndex.abs();
}
newIndex = newIndex % secret.length;
res[targetIndex] = secret[newIndex];
// Swap secret[newIndex] with last element, then shrink
final lastSecretIndex = secret.length - 1;
final a = secret[lastSecretIndex];
final b = secret[newIndex];
secret[newIndex] = a;
secret[lastSecretIndex] = b;
secret = secret.sublist(0, lastSecretIndex);
add = !add;
timeDigitIndex--;
if (timeDigitIndex < 0) {
timeDigitIndex = timeDigits.length - 1;
}
}
final resStr = res.join('');
final pipeIndex = resStr.indexOf('|');
if (pipeIndex < 0) return null;
final timestampPart = resStr.substring(0, pipeIndex);
if (timestampPart != timeNowBackup.toString()) return null;
return resStr.substring(pipeIndex + 1);
}
}
+45
View File
@@ -0,0 +1,45 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
/// 视频全屏状态通知器,用于在 main.dart 中隐藏桌面端标题栏
final videoFullscreenNotifier = ValueNotifier<bool>(false);
/// 进入视频全屏(系统级)
Future<void> enterVideoFullscreen() async {
videoFullscreenNotifier.value = true;
if (Platform.isAndroid || Platform.isIOS) {
await Future.wait([
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
),
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]),
]);
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
await windowManager.setFullScreen(true);
}
}
/// 退出视频全屏(系统级)
Future<void> exitVideoFullscreen() async {
videoFullscreenNotifier.value = false;
if (Platform.isAndroid || Platform.isIOS) {
await Future.wait([
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
),
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]),
]);
} else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
await windowManager.setFullScreen(false);
}
}
+44
View File
@@ -0,0 +1,44 @@
/// 字符串验证器
class StringValidator {
/// 验证邮箱
static String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return '请输入邮箱';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
}
/// 验证密码
static String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码长度至少为6位';
}
return null;
}
/// 验证必填
static String? validateRequired(String? value, {String message = '此字段不能为空'}) {
if (value == null || value.isEmpty) {
return message;
}
return null;
}
/// 验证昵称
static String? validateNickname(String? value) {
if (value == null || value.isEmpty) {
return '请输入昵称';
}
if (value.length > 50) {
return '昵称长度不能超过50个字符';
}
return null;
}
}