二次开发源码提交

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
+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);
}
}