二次开发源码提交
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 '文件';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user