Files

352 lines
9.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:intl/intl.dart';
/// Cloud file model representing a file or folder in the cloud drive.
class CloudFile {
static const supportedVideoExtensions = {
'mp4',
'm4v',
'mkv',
'mov',
'avi',
'ts',
'm2ts',
'mts',
'webm',
'flv',
'f4v',
'wmv',
'asf',
'mpg',
'mpeg',
'vob',
'iso',
'rm',
'rmvb',
'3gp',
'ogv',
};
final String id;
final String name;
final bool isDirectory;
final int? size;
final String? gcid;
final int? subDirectoryCount;
final int? subFileCount;
final String modifiedAt;
final String cloudPath;
/// Stable parent directory ID from the cloud API, used for directory-scoped
/// operations even when the API omits a full display path.
final String? parentID;
final String? fullParentIDs;
final int fileType;
const CloudFile({
required this.id,
required this.name,
required this.isDirectory,
this.size,
this.gcid,
this.subDirectoryCount,
this.subFileCount,
this.modifiedAt = '',
this.cloudPath = '',
this.parentID,
this.fullParentIDs,
this.fileType = 0,
});
bool get isVideo {
if (isDirectory) return false;
if (fileType == 2) return true;
final ext = name.split('.').last.toLowerCase();
return supportedVideoExtensions.contains(ext);
}
bool get isImage => fileType == 1;
bool get isAudio => fileType == 3;
bool get isDocument => fileType == 4;
String get icon {
if (isDirectory) return 'folder';
if (isVideo) return 'movie';
switch (fileType) {
case 1:
return 'image';
case 2:
return 'movie';
case 3:
return 'music_note';
case 4:
return 'description';
case 5:
case 9:
return 'archive';
default:
return 'insert_drive_file';
}
}
String get typeName {
if (isDirectory) return '文件夹';
if (isVideo) return '视频';
const names = {1: '图片', 2: '视频', 3: '音频', 4: '文档', 5: '压缩包', 9: 'BT种子'};
return names[fileType] ?? '文件';
}
String get formattedSize {
if (size == null) return '--';
return _formatBytes(size!);
}
/// Finder 列表中目录名称下方显示的子项统计。
String? get directoryContentSummary {
if (!isDirectory) return null;
final counts = <String>[];
if (subDirectoryCount != null) {
counts.add(
'${NumberFormat.decimalPattern('zh_CN').format(subDirectoryCount)} 个文件夹',
);
}
if (subFileCount != null) {
counts.add(
'${NumberFormat.decimalPattern('zh_CN').format(subFileCount)} 个文件',
);
}
return counts.isEmpty ? '文件夹' : '文件夹 · ${counts.join('')}';
}
CloudFile copyWith({
String? id,
String? name,
bool? isDirectory,
int? size,
bool clearSize = false,
String? gcid,
int? subDirectoryCount,
int? subFileCount,
String? modifiedAt,
String? cloudPath,
String? parentID,
String? fullParentIDs,
int? fileType,
}) {
return CloudFile(
id: id ?? this.id,
name: name ?? this.name,
isDirectory: isDirectory ?? this.isDirectory,
size: clearSize ? null : (size ?? this.size),
gcid: gcid ?? this.gcid,
subDirectoryCount: subDirectoryCount ?? this.subDirectoryCount,
subFileCount: subFileCount ?? this.subFileCount,
modifiedAt: modifiedAt ?? this.modifiedAt,
cloudPath: cloudPath ?? this.cloudPath,
parentID: parentID ?? this.parentID,
fullParentIDs: fullParentIDs ?? this.fullParentIDs,
fileType: fileType ?? this.fileType,
);
}
static String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
if (bytes < 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(2)} TB';
}
return '${(bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toStringAsFixed(2)} PB';
}
factory CloudFile.fromJson(Map<String, dynamic> json) {
final name =
json['name'] ??
json['fileName'] ??
json['file_name'] ??
json['resName'] ??
json['res_name'] ??
json['dirName'] ??
json['dir_name'] ??
'';
final id = _extractId(json);
if (id.isEmpty) throw FormatException('Missing file ID');
final resourceType = _extractInt(json, ['resType']);
final type = _extractInt(json, ['fileType', 'type']) ?? 0;
bool isDir;
final explicitDir = json['isDir'] ?? json['dir'] ?? json['directoryType'];
if (explicitDir != null) {
isDir = _truthyDirectoryFlag(explicitDir);
} else if (resourceType != null) {
isDir = resourceType == 2;
} else {
isDir =
type == 0 && (json['dirName'] != null || json['children'] != null);
}
int? fileSize = _extractIntDeep(json, [
'size',
'fileSize',
'resSize',
'totalSize',
'dirSize',
'folderSize',
]);
if (isDir && fileSize == null) fileSize = 0;
final epoch = _extractIntDeep(json, ['utime', 'ctime']);
return CloudFile(
id: id,
name: name.toString(),
isDirectory: isDir,
size: fileSize,
gcid: _extractStringDeep(json, ['gcid', 'gcId', 'gcidValue', 'hash']),
subDirectoryCount: _extractIntDeep(json, [
'subDirCount',
'subDirectoryCount',
'directoryCount',
'dirCount',
]),
subFileCount: _extractIntDeep(json, [
'subFileCount',
'subFileNum',
'fileCount',
]),
modifiedAt:
json['updateTime']?.toString() ??
json['updatedAt']?.toString() ??
json['modifyTime']?.toString() ??
json['createTime']?.toString() ??
(epoch == null ? null : _formatEpoch(epoch)) ??
'',
cloudPath:
_extractString(json, ['location', 'path', 'fullPath']) ??
name.toString(),
parentID: _extractString(json, ['parentId', 'parent_id', 'parentID']),
fullParentIDs: _extractString(json, [
'fullParentIds',
'fullParentIDs',
'full_parent_ids',
]),
fileType: type,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'isDir': isDirectory,
'size': size,
'gcid': gcid,
'subDirCount': subDirectoryCount,
'subFileCount': subFileCount,
'updateTime': modifiedAt,
'path': cloudPath,
'parentId': parentID,
'fullParentIds': fullParentIDs,
'fileType': fileType,
};
static String _extractId(Map<String, dynamic> json) {
for (final key in ['fileId', 'file_id', 'resId', 'res_id', 'fid', 'id']) {
final v = json[key];
if (v != null && v.toString().isNotEmpty) return v.toString();
}
return '';
}
static String? _extractString(Map<String, dynamic> json, List<String> keys) {
for (final key in keys) {
final v = json[key];
if (v != null) return v.toString();
}
return null;
}
static int? _extractInt(Map<String, dynamic> json, List<String> keys) {
for (final key in keys) {
final v = json[key];
final value = _toInt(v);
if (value != null) return value;
}
return null;
}
static String? _extractStringDeep(
Map<String, dynamic> json,
List<String> keys,
) {
final direct = _extractString(json, keys);
if (direct != null && direct.isNotEmpty) return direct;
for (final entry in json.entries) {
final value = entry.value;
if (value is Map<String, dynamic>) {
final found = _extractStringDeep(value, keys);
if (found != null && found.isNotEmpty) return found;
} else if (value is List) {
for (final item in value) {
if (item is Map<String, dynamic>) {
final found = _extractStringDeep(item, keys);
if (found != null && found.isNotEmpty) return found;
}
}
}
}
return null;
}
static int? _extractIntDeep(Map<String, dynamic> json, List<String> keys) {
final direct = _extractInt(json, keys);
if (direct != null) return direct;
for (final entry in json.entries) {
final value = entry.value;
if (value is Map<String, dynamic>) {
final found = _extractIntDeep(value, keys);
if (found != null) return found;
} else if (value is List) {
for (final item in value) {
if (item is Map<String, dynamic>) {
final found = _extractIntDeep(item, keys);
if (found != null) return found;
}
}
}
}
return null;
}
static int? _toInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is double) return value.toInt();
return int.tryParse(value.toString());
}
static bool _truthyDirectoryFlag(dynamic value) {
if (value is bool) return value;
if (value is num) return value == 1;
final text = value.toString().toLowerCase();
return text == '1' || text == 'true' || text == 'folder' || text == 'dir';
}
static String _formatEpoch(int epoch) {
final seconds = epoch > 9999999999 ? epoch ~/ 1000 : epoch;
return DateFormat(
'yyyy-MM-dd HH:mm',
).format(DateTime.fromMillisecondsSinceEpoch(seconds * 1000));
}
@override
bool operator ==(Object other) =>
identical(this, other) || other is CloudFile && id == other.id;
@override
int get hashCode => id.hashCode;
}