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!); } CloudFile copyWith({ String? id, String? name, bool? isDirectory, int? size, bool clearSize = false, String? gcid, int? subDirectoryCount, int? subFileCount, String? modifiedAt, String? cloudPath, 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, 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'; } return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } factory CloudFile.fromJson(Map 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, ); } Map toJson() => { 'id': id, 'name': name, 'isDir': isDirectory, 'size': size, 'gcid': gcid, 'subDirCount': subDirectoryCount, 'subFileCount': subFileCount, 'updateTime': modifiedAt, 'path': cloudPath, 'fileType': fileType, }; static String _extractId(Map 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 json, List keys) { for (final key in keys) { final v = json[key]; if (v != null) return v.toString(); } return null; } static int? _extractInt(Map json, List 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 json, List 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) { 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) { final found = _extractStringDeep(item, keys); if (found != null && found.isNotEmpty) return found; } } } } return null; } static int? _extractIntDeep(Map json, List 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) { final found = _extractIntDeep(value, keys); if (found != null) return found; } else if (value is List) { for (final item in value) { if (item is Map) { 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; }