Files
guangya_flutter/lib/models/cloud_file.dart
T
2026-07-18 10:23:34 +08:00

261 lines
6.9 KiB
Dart

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;
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.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!);
}
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';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
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']),
subFileCount: _extractIntDeep(json, ['subFileCount']),
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(),
fileType: type,
);
}
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;
}