二次开发源码提交

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
+198
View File
@@ -0,0 +1,198 @@
// 管理员相关数据模型
class PaginationModel {
final int page;
final int pageSize;
final int totalItems;
PaginationModel({
required this.page,
required this.pageSize,
required this.totalItems,
});
factory PaginationModel.fromJson(Map<String, dynamic> json) {
return PaginationModel(
page: json['page'] as int? ?? 0,
pageSize: json['page_size'] as int? ?? 10,
totalItems: json['total_items'] as int? ?? 0,
);
}
}
/// 管理员用户组模型
class AdminGroupModel {
final int id;
final String name;
final String? permissions;
final Map<String, dynamic>? settings;
final int? maxStorage;
final int? storagePolicyId;
final DateTime? createdAt;
final DateTime? updatedAt;
final Map<String, dynamic>? storagePolicy;
final int? totalUsers;
AdminGroupModel({
required this.id,
required this.name,
this.permissions,
this.settings,
this.maxStorage,
this.storagePolicyId,
this.createdAt,
this.updatedAt,
this.storagePolicy,
this.totalUsers,
});
factory AdminGroupModel.fromJson(Map<String, dynamic> json) {
Map<String, dynamic>? storagePolicy;
if (json['edges'] is Map && json['edges']['storage_policies'] is Map) {
storagePolicy = json['edges']['storage_policies'] as Map<String, dynamic>;
}
return AdminGroupModel(
id: json['id'] as int,
name: json['name'] as String,
permissions: json['permissions'] as String?,
settings: json['settings'] as Map<String, dynamic>?,
maxStorage: json['max_storage'] as int?,
storagePolicyId: json['storage_policy_id'] as int?,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: null,
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
storagePolicy: storagePolicy,
totalUsers: json['total_users'] as int?,
);
}
/// 格式化最大存储空间
String get formattedMaxStorage {
if (maxStorage == null) return '无限制';
return _formatBytes(maxStorage!);
}
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';
}
}
/// 管理员用户模型
class AdminUserModel {
final int id;
final String email;
final String nick;
final String status;
final String? avatar;
final int? storage;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? hashId;
final int? groupUsers;
final AdminGroupModel? group;
AdminUserModel({
required this.id,
required this.email,
required this.nick,
required this.status,
this.avatar,
this.storage,
this.createdAt,
this.updatedAt,
this.hashId,
this.groupUsers,
this.group,
});
factory AdminUserModel.fromJson(Map<String, dynamic> json) {
AdminGroupModel? group;
if (json['edges'] is Map && json['edges']['group'] is Map) {
group = AdminGroupModel.fromJson(
json['edges']['group'] as Map<String, dynamic>);
}
return AdminUserModel(
id: json['id'] as int,
email: json['email'] as String? ?? '',
nick: json['nick'] as String? ?? '',
status: json['status'] as String? ?? 'active',
avatar: json['avatar'] as String?,
storage: json['storage'] as int?,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: null,
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
hashId: json['hash_id'] as String?,
groupUsers: json['group_users'] as int?,
group: group,
);
}
/// 获取用户头像首字母
String get initial => nick.isNotEmpty ? nick[0].toUpperCase() : 'U';
/// 格式化已用存储空间
String get formattedStorage {
if (storage == null) return '未知';
return _formatBytes(storage!);
}
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';
}
}
/// 管理员用户组列表响应
class AdminGroupListResponse {
final List<AdminGroupModel> groups;
final PaginationModel pagination;
AdminGroupListResponse({required this.groups, required this.pagination});
factory AdminGroupListResponse.fromJson(Map<String, dynamic> json) {
return AdminGroupListResponse(
groups: (json['groups'] as List?)
?.map((e) => AdminGroupModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
pagination: PaginationModel.fromJson(
json['pagination'] as Map<String, dynamic>? ?? {}),
);
}
}
/// 管理员用户列表响应
class AdminUserListResponse {
final List<AdminUserModel> users;
final PaginationModel pagination;
AdminUserListResponse({required this.users, required this.pagination});
factory AdminUserListResponse.fromJson(Map<String, dynamic> json) {
return AdminUserListResponse(
users: (json['users'] as List?)
?.map((e) => AdminUserModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
pagination: PaginationModel.fromJson(
json['pagination'] as Map<String, dynamic>? ?? {}),
);
}
}
+96
View File
@@ -0,0 +1,96 @@
/// 缓存设置模型
class CacheSettingsModel {
/// 最大缓存大小(字节)
final int maxCacheSize;
/// 缓存过期时间(毫秒)
final int cacheExpireDuration;
/// 是否自动清理最旧文件
final bool autoCleanOldFiles;
CacheSettingsModel({
this.maxCacheSize = 500 * 1024 * 1024, // 默认500MB
this.cacheExpireDuration = 7 * 24 * 60 * 60 * 1000, // 默认7天
this.autoCleanOldFiles = true,
});
factory CacheSettingsModel.fromJson(Map<String, dynamic> json) {
return CacheSettingsModel(
maxCacheSize: json['max_cache_size'] as int? ?? 500 * 1024 * 1024,
cacheExpireDuration:
json['cache_expire_duration'] as int? ?? 7 * 24 * 60 * 60 * 1000,
autoCleanOldFiles: json['auto_clean_old_files'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'max_cache_size': maxCacheSize,
'cache_expire_duration': cacheExpireDuration,
'auto_clean_old_files': autoCleanOldFiles,
};
}
/// 获取人类可读的最大缓存大小
String get maxCacheSizeReadable {
return _formatBytes(maxCacheSize);
}
/// 获取人类可读的缓存过期时间
String get cacheExpireDurationReadable {
final days = cacheExpireDuration ~/ (24 * 60 * 60 * 1000);
if (days > 0) {
return '$days天';
}
final hours = cacheExpireDuration ~/ (60 * 60 * 1000);
if (hours > 0) {
return '$hours小时';
}
final minutes = cacheExpireDuration ~/ (60 * 1000);
return '$minutes分钟';
}
String _formatBytes(int bytes) {
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
/// 从MB创建
static CacheSettingsModel fromMB(int maxCacheSizeMB) {
return CacheSettingsModel(maxCacheSize: maxCacheSizeMB * 1024 * 1024);
}
/// 从天数创建
static CacheSettingsModel fromDays(int days) {
return CacheSettingsModel(
cacheExpireDuration: days * 24 * 60 * 60 * 1000,
);
}
/// 创建副本
CacheSettingsModel copyWith({
int? maxCacheSize,
int? cacheExpireDuration,
bool? autoCleanOldFiles,
}) {
return CacheSettingsModel(
maxCacheSize: maxCacheSize ?? this.maxCacheSize,
cacheExpireDuration: cacheExpireDuration ?? this.cacheExpireDuration,
autoCleanOldFiles: autoCleanOldFiles ?? this.autoCleanOldFiles,
);
}
/// 获取可用预设大小选项
static List<int> get availableSizes => [100, 250, 500, 1000, 2000]; // MB
/// 获取可用预设过期时间选项
static List<int> get availableDurations => [1, 3, 7, 15, 30]; // 天
}
+40
View File
@@ -0,0 +1,40 @@
/// WebDAV 账户模型
class DavAccountModel {
final String id;
final DateTime createdAt;
final String name;
final String uri;
final String password;
final String options;
DavAccountModel({
required this.id,
required this.createdAt,
required this.name,
required this.uri,
required this.password,
required this.options,
});
factory DavAccountModel.fromJson(Map<String, dynamic> json) {
return DavAccountModel(
id: json['id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
name: json['name'] as String,
uri: json['uri'] as String,
password: json['password'] as String,
options: json['options'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'created_at': createdAt.toIso8601String(),
'name': name,
'uri': uri,
'password': password,
'options': options,
};
}
}
+154
View File
@@ -0,0 +1,154 @@
import '../../core/utils/app_logger.dart';
/// 下载状态
enum DownloadStatus {
waiting, // 等待中
downloading, // 下载中
completed, // 已完成
paused, // 已暂停
failed, // 失败
cancelled, // 已取消
}
/// 下载任务模型
class DownloadTaskModel {
final String id;
final String fileName;
final String fileUri; // cloudreve URI
final String? downloadUrl; // 实际下载URL
final int fileSize;
final String savePath;
String? backgroundTaskId; // background_downloader 的 task ID(可变,用于重启后恢复映射)
DownloadStatus status;
int downloadedBytes;
int speed; // 下载速度,字节/秒
bool waitingForWifi; // 是否在等待WiFi(非持久化)
final DateTime createdAt;
DateTime? completedAt;
String? errorMessage;
double get progress => fileSize > 0 ? downloadedBytes / fileSize : 0.0;
int get remainingBytes => fileSize - downloadedBytes;
String get speedText {
if (speed <= 0) return '';
if (speed < 1024) return '$speed B/s';
if (speed < 1024 * 1024) return '${(speed / 1024).toStringAsFixed(1)} KB/s';
return '${(speed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
}
String get progressText {
if (status == DownloadStatus.completed) {
return '100%';
}
AppLogger.d('DownloadTaskModel -> 已经初始化, status: $status');
final percent = (progress * 100).toStringAsFixed(1);
AppLogger.d('DownloadTaskModel -> 已经初始化, percent: $percent');
return '$percent%';
}
String get statusText {
switch (status) {
case DownloadStatus.waiting:
return waitingForWifi ? '等待WiFi' : '等待中';
case DownloadStatus.downloading:
return '下载中';
case DownloadStatus.completed:
return '已完成';
case DownloadStatus.paused:
return '已暂停';
case DownloadStatus.failed:
return '下载失败';
case DownloadStatus.cancelled:
return '已取消';
}
}
DownloadTaskModel({
required this.id,
required this.fileName,
required this.fileUri,
this.downloadUrl,
required this.fileSize,
required this.savePath,
this.backgroundTaskId,
this.status = DownloadStatus.waiting,
this.downloadedBytes = 0,
this.speed = 0,
this.waitingForWifi = false,
DateTime? createdAt,
this.completedAt,
this.errorMessage,
}) : createdAt = createdAt ?? DateTime.now();
DownloadTaskModel copyWith({
String? id,
String? fileName,
String? fileUri,
String? downloadUrl,
int? fileSize,
String? savePath,
String? backgroundTaskId,
DownloadStatus? status,
int? downloadedBytes,
int? speed,
bool? waitingForWifi,
DateTime? createdAt,
DateTime? completedAt,
String? errorMessage,
}) {
return DownloadTaskModel(
id: id ?? this.id,
fileName: fileName ?? this.fileName,
fileUri: fileUri ?? this.fileUri,
downloadUrl: downloadUrl ?? this.downloadUrl,
fileSize: fileSize ?? this.fileSize,
savePath: savePath ?? this.savePath,
backgroundTaskId: backgroundTaskId ?? this.backgroundTaskId,
status: status ?? this.status,
downloadedBytes: downloadedBytes ?? this.downloadedBytes,
speed: speed ?? this.speed,
waitingForWifi: waitingForWifi ?? this.waitingForWifi,
createdAt: createdAt ?? this.createdAt,
completedAt: completedAt ?? this.completedAt,
errorMessage: errorMessage ?? this.errorMessage,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'fileName': fileName,
'fileUri': fileUri,
'downloadUrl': downloadUrl,
'fileSize': fileSize,
'savePath': savePath,
'backgroundTaskId': backgroundTaskId,
'status': status.index,
'downloadedBytes': downloadedBytes,
'speed': speed,
'createdAt': createdAt.toIso8601String(),
'completedAt': completedAt?.toIso8601String(),
'errorMessage': errorMessage,
};
}
factory DownloadTaskModel.fromJson(Map<String, dynamic> json) {
return DownloadTaskModel(
id: json['id'] as String,
fileName: json['fileName'] as String,
fileUri: json['fileUri'] as String,
downloadUrl: json['downloadUrl'] as String?,
fileSize: json['fileSize'] as int,
savePath: json['savePath'] as String,
backgroundTaskId: json['backgroundTaskId'] as String?,
status: DownloadStatus.values[json['status'] as int? ?? 0],
downloadedBytes: json['downloadedBytes'] as int? ?? 0,
speed: json['speed'] as int? ?? 0,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
completedAt: json['completedAt'] != null
? DateTime.parse(json['completedAt'] as String)
: null,
errorMessage: json['errorMessage'] as String?,
);
}
}
+375
View File
@@ -0,0 +1,375 @@
import 'share_model.dart';
/// 文件模型
class FileModel {
final int type; // 0:文件, 1:文件夹
final String id;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
final int size;
final String path;
final Map<String, dynamic>? metadata;
final String? permission;
final String? primaryEntity;
final String? capability;
final bool? owned;
FileModel({
required this.type,
required this.id,
required this.name,
required this.createdAt,
required this.updatedAt,
required this.size,
required this.path,
this.metadata,
this.permission,
this.primaryEntity,
this.capability,
this.owned,
});
factory FileModel.fromJson(Map<String, dynamic> json) {
return FileModel(
type: json['type'] as int,
id: json['id'] as String,
name: json['name'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
size: (json['size'] as num?)?.toInt() ?? 0,
path: json['path'] as String,
metadata: json['metadata'] as Map<String, dynamic>?,
permission: json['permission'] as String?,
primaryEntity: json['primary_entity'] as String?,
capability: json['capability'] as String?,
owned: json['owned'] as bool?,
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'id': id,
'name': name,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'size': size,
'path': path,
'metadata': metadata,
'permission': permission,
'primary_entity': primaryEntity,
'capability': capability,
'owned': owned,
};
}
bool get isFile => type == 0;
bool get isFolder => type == 1;
FileModel copyWith({
int? type,
String? id,
String? name,
DateTime? createdAt,
DateTime? updatedAt,
int? size,
String? path,
Map<String, dynamic>? metadata,
String? permission,
String? primaryEntity,
String? capability,
bool? owned,
}) {
return FileModel(
type: type ?? this.type,
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
size: size ?? this.size,
path: path ?? this.path,
metadata: metadata ?? this.metadata,
permission: permission ?? this.permission,
primaryEntity: primaryEntity ?? this.primaryEntity,
capability: capability ?? this.capability,
owned: owned ?? this.owned,
);
}
/// 获取相对于 cloudreve://my 的路径
/// 例如: cloudreve://my/Games -> /Games
/// cloudreve://my/sub/folder -> /sub/folder
String get relativePath {
if (!path.startsWith('cloudreve://my')) {
// 如果不是 cloudreve://my 开头,返回空
return '/';
}
final prefix = 'cloudreve://my';
final relative = path.substring(prefix.length);
return relative.isEmpty ? '/' : relative;
}
}
/// 文件夹摘要模型
class FolderSummaryModel {
final int size;
final int files;
final int folders;
final bool completed;
final DateTime calculatedAt;
FolderSummaryModel({
required this.size,
required this.files,
required this.folders,
required this.completed,
required this.calculatedAt,
});
factory FolderSummaryModel.fromJson(Map<String, dynamic> json) {
return FolderSummaryModel(
size: (json['size'] as num?)?.toInt() ?? 0,
files: (json['files'] as num?)?.toInt() ?? 0,
folders: (json['folders'] as num?)?.toInt() ?? 0,
completed: json['completed'] as bool,
calculatedAt: DateTime.parse(json['calculated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'size': size,
'files': files,
'folders': folders,
'completed': completed,
'calculated_at': calculatedAt.toIso8601String(),
};
}
}
/// 扩展信息模型
class ExtendedInfoModel {
final StoragePolicyModel? storagePolicy;
final int? storageUsed;
final List<ShareModel>? shares;
final List<EntityModel>? entities;
final List<DirectLinkModel>? directLinks;
ExtendedInfoModel({
this.storagePolicy,
this.storageUsed,
this.shares,
this.entities,
this.directLinks,
});
factory ExtendedInfoModel.fromJson(Map<String, dynamic> json) {
return ExtendedInfoModel(
storagePolicy: json['storage_policy'] is Map<String, dynamic>
? StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>)
: null,
storageUsed: (json['storage_used'] as num?)?.toInt(),
shares: json['shares'] != null
? (json['shares'] as List)
.map((e) => ShareModel.fromJson(e as Map<String, dynamic>))
.toList()
: null,
entities: json['entities'] != null
? (json['entities'] as List)
.map((e) => EntityModel.fromJson(e as Map<String, dynamic>))
.toList()
: null,
directLinks: json['direct_links'] != null
? (json['direct_links'] as List)
.map((e) => DirectLinkModel.fromJson(e as Map<String, dynamic>))
.toList()
: null,
);
}
Map<String, dynamic> toJson() {
return {
'storage_policy': storagePolicy?.toJson(),
'storage_used': storageUsed,
'shares': shares?.map((e) => e.toJson()).toList(),
'entities': entities?.map((e) => e.toJson()).toList(),
'direct_links': directLinks?.map((e) => e.toJson()).toList(),
};
}
}
/// 存储策略模型
class StoragePolicyModel {
final String id;
final String name;
final String type;
final int? maxSize;
StoragePolicyModel({
required this.id,
required this.name,
required this.type,
this.maxSize,
});
factory StoragePolicyModel.fromJson(Map<String, dynamic> json) {
return StoragePolicyModel(
id: json['id'] as String,
name: json['name'] as String,
type: json['type'] as String,
maxSize: (json['max_size'] as num?)?.toInt(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'type': type,
'max_size': maxSize,
};
}
}
/// 实体创建者模型
class EntityCreatedByModel {
final String id;
final String nickname;
final String? avatar;
final DateTime createdAt;
EntityCreatedByModel({
required this.id,
required this.nickname,
this.avatar,
required this.createdAt,
});
factory EntityCreatedByModel.fromJson(Map<String, dynamic> json) {
return EntityCreatedByModel(
id: json['id'] as String,
nickname: json['nickname'] as String,
avatar: json['avatar'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'nickname': nickname,
'avatar': avatar,
'created_at': createdAt.toIso8601String(),
};
}
}
/// 实体模型(文件版本/Blob)
class EntityModel {
final String id;
final int type;
final DateTime createdAt;
final int size;
final String? encryptedWith;
final StoragePolicyModel? storagePolicy;
final EntityCreatedByModel? createdBy;
EntityModel({
required this.id,
required this.type,
required this.createdAt,
required this.size,
this.encryptedWith,
this.storagePolicy,
this.createdBy,
});
factory EntityModel.fromJson(Map<String, dynamic> json) {
return EntityModel(
id: json['id'] as String,
type: json['type'] as int,
createdAt: DateTime.parse(json['created_at'] as String),
size: (json['size'] as num?)?.toInt() ?? 0,
encryptedWith: json['encrypted_with'] as String?,
storagePolicy: json['storage_policy'] is Map<String, dynamic>
? StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>)
: null,
createdBy: json['created_by'] is Map<String, dynamic>
? EntityCreatedByModel.fromJson(json['created_by'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'created_at': createdAt.toIso8601String(),
'size': size,
'encrypted_with': encryptedWith,
'storage_policy': storagePolicy?.toJson(),
'created_by': createdBy?.toJson(),
};
}
}
/// 直链模型
class DirectLinkModel {
final String id;
final DateTime createdAt;
final String url;
final int downloaded;
DirectLinkModel({
required this.id,
required this.createdAt,
required this.url,
required this.downloaded,
});
factory DirectLinkModel.fromJson(Map<String, dynamic> json) {
return DirectLinkModel(
id: json['id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
url: json['url'] as String,
downloaded: (json['downloaded'] as num?)?.toInt() ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'created_at': createdAt.toIso8601String(),
'url': url,
'downloaded': downloaded,
};
}
}
/// 文件详情模型(/file/info 接口返回)
class FileInfoModel {
final FileModel file;
final FolderSummaryModel? folderSummary;
final ExtendedInfoModel? extendedInfo;
FileInfoModel({
required this.file,
this.folderSummary,
this.extendedInfo,
});
factory FileInfoModel.fromJson(Map<String, dynamic> json) {
return FileInfoModel(
file: FileModel.fromJson(json),
folderSummary: json['folder_summary'] is Map<String, dynamic>
? FolderSummaryModel.fromJson(json['folder_summary'] as Map<String, dynamic>)
: null,
extendedInfo: json['extended_info'] is Map<String, dynamic>
? ExtendedInfoModel.fromJson(json['extended_info'] as Map<String, dynamic>)
: null,
);
}
}
@@ -0,0 +1,301 @@
/// 离线下载任务状态
enum RemoteDownloadStatus {
queued,
running,
completed,
error,
suspending,
suspended;
static RemoteDownloadStatus fromString(String value) {
return RemoteDownloadStatus.values.firstWhere(
(e) => e.name == value,
orElse: () => RemoteDownloadStatus.queued,
);
}
String get text {
switch (this) {
case RemoteDownloadStatus.queued:
return '排队中';
case RemoteDownloadStatus.running:
return '下载中';
case RemoteDownloadStatus.completed:
return '已完成';
case RemoteDownloadStatus.error:
return '出错';
case RemoteDownloadStatus.suspending:
return '暂停中';
case RemoteDownloadStatus.suspended:
return '已暂停';
}
}
bool get isOngoing =>
this == RemoteDownloadStatus.queued ||
this == RemoteDownloadStatus.running ||
this == RemoteDownloadStatus.suspending ||
this == RemoteDownloadStatus.suspended;
}
/// 种子文件信息
class DownloadFileInfo {
final int index;
final String name;
final int size;
final double progress;
final bool selected;
const DownloadFileInfo({
required this.index,
required this.name,
required this.size,
required this.progress,
required this.selected,
});
factory DownloadFileInfo.fromJson(Map<String, dynamic> json) {
return DownloadFileInfo(
index: json['index'] as int,
name: json['name'] as String,
size: json['size'] as int,
progress: (json['progress'] as num).toDouble(),
selected: json['selected'] as bool? ?? true,
);
}
}
/// 下载详情(aria2 返回的信息)
class DownloadInfo {
final String name;
final String state;
final int total;
final int downloaded;
final int downloadSpeed;
final int uploaded;
final int uploadSpeed;
final String hash;
final List<DownloadFileInfo> files;
final int numPieces;
const DownloadInfo({
required this.name,
required this.state,
required this.total,
required this.downloaded,
required this.downloadSpeed,
required this.uploaded,
required this.uploadSpeed,
required this.hash,
required this.files,
this.numPieces = 0,
});
double get progress => total > 0 ? downloaded / total : 0.0;
String get speedText {
if (downloadSpeed <= 0) return '';
if (downloadSpeed < 1024) return '$downloadSpeed B/s';
if (downloadSpeed < 1024 * 1024) {
return '${(downloadSpeed / 1024).toStringAsFixed(1)} KB/s';
}
return '${(downloadSpeed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
}
factory DownloadInfo.fromJson(Map<String, dynamic> json) {
final filesList = json['files'] as List<dynamic>? ?? [];
return DownloadInfo(
name: json['name'] as String? ?? '',
state: json['state'] as String? ?? '',
total: (json['total'] as num?)?.toInt() ?? 0,
downloaded: (json['downloaded'] as num?)?.toInt() ?? 0,
downloadSpeed: (json['download_speed'] as num?)?.toInt() ?? 0,
uploaded: (json['uploaded'] as num?)?.toInt() ?? 0,
uploadSpeed: (json['upload_speed'] as num?)?.toInt() ?? 0,
hash: json['hash'] as String? ?? '',
files: filesList
.map((f) => DownloadFileInfo.fromJson(f as Map<String, dynamic>))
.toList(),
numPieces: (json['num_pieces'] as num?)?.toInt() ?? 0,
);
}
}
/// 处理节点信息
class TaskNode {
final String id;
final String name;
final String type;
const TaskNode({
required this.id,
required this.name,
required this.type,
});
String get displayName {
switch (type) {
case 'master':
return '$name(本机)';
default:
return name;
}
}
factory TaskNode.fromJson(Map<String, dynamic> json) {
return TaskNode(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
type: json['type'] as String? ?? '',
);
}
}
/// 任务摘要
class TaskSummary {
final String phase;
final String dst;
final int failed;
final String src;
final String srcStr;
final DownloadInfo? download;
const TaskSummary({
required this.phase,
required this.dst,
required this.failed,
required this.src,
required this.srcStr,
this.download,
});
factory TaskSummary.fromJson(Map<String, dynamic> json) {
return TaskSummary(
phase: json['phase'] as String? ?? '',
dst: json['props']?['dst'] as String? ?? '',
failed: (json['props']?['failed'] as num?)?.toInt() ?? 0,
src: json['props']?['src'] as String? ?? '',
srcStr: json['props']?['src_str'] as String? ?? '',
download: json['props']?['download'] != null
? DownloadInfo.fromJson(
json['props']!['download'] as Map<String, dynamic>)
: null,
);
}
}
/// 离线下载任务模型
class RemoteDownloadTaskModel {
final String id;
final RemoteDownloadStatus status;
final String type;
final DateTime createdAt;
final DateTime updatedAt;
final int duration;
final String? error;
final TaskNode? node;
final TaskSummary? summary;
final int resumeTime;
final int retryCount;
const RemoteDownloadTaskModel({
required this.id,
required this.status,
required this.type,
required this.createdAt,
required this.updatedAt,
required this.duration,
this.error,
this.node,
this.summary,
this.resumeTime = 0,
this.retryCount = 0,
});
String getFileNameFromUrl(String url) {
if (url.isEmpty) return '';
try {
final uri = Uri.parse(url);
final segments = uri.pathSegments;
if (segments.isEmpty) return '';
final fileName = segments.last;
return fileName.isNotEmpty ? fileName : '';
} catch (e) {
return '';
}
}
/// 显示名称:优先使用 download.name,其次 srcStr,最后 id
String get displayName {
final dlName = summary?.download?.name;
if (dlName != null && dlName.isNotEmpty) {
return dlName;
} else {
final srcStr = summary?.srcStr;
if (srcStr == null || srcStr.isEmpty) return id;
final fileName = getFileNameFromUrl(srcStr);
if (fileName.isEmpty) return id;
return fileName;
}
}
/// 格式化耗时(duration 单位为毫秒)
String get durationText {
final seconds = duration ~/ 1000;
if (seconds <= 0) return '-';
if (seconds < 60) return '$seconds';
if (seconds < 3600) return '${seconds ~/ 60}${seconds % 60}';
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
return '$hours 小时 $minutes';
}
/// 输入源显示文本
String get srcDisplayText {
final srcStr = summary?.srcStr;
if (srcStr != null && srcStr.isNotEmpty) return srcStr;
final src = summary?.src;
if (src!.isNotEmpty) {
// 从 cloudreve URI 提取文件名
final parts = src.split('/');
return parts.isNotEmpty ? parts.last : src;
}
return '-';
}
/// 输出目标显示文本(从 cloudreve URI 转换为相对路径)
String get dstDisplayText {
final dst = summary?.dst ?? '';
if (dst.isEmpty) return '-';
if (dst.startsWith('cloudreve://my')) {
final relative = dst.replaceFirst('cloudreve://my', '');
return relative.isEmpty ? '/' : relative;
}
return dst;
}
factory RemoteDownloadTaskModel.fromJson(Map<String, dynamic> json) {
return RemoteDownloadTaskModel(
id: json['id'] as String,
status: RemoteDownloadStatus.fromString(json['status'] as String),
type: json['type'] as String? ?? '',
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String).toLocal()
: DateTime.now(),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String).toLocal()
: DateTime.now(),
duration: (json['duration'] as num?)?.toInt() ?? 0,
error: json['error'] as String?,
node: json['node'] != null
? TaskNode.fromJson(json['node'] as Map<String, dynamic>)
: null,
summary: json['summary'] != null
? TaskSummary.fromJson(json['summary'] as Map<String, dynamic>)
: null,
resumeTime: (json['resume_time'] as num?)?.toInt() ?? 0,
retryCount: (json['retry_count'] as num?)?.toInt() ?? 0,
);
}
}
+62
View File
@@ -0,0 +1,62 @@
import 'package:cloudreve4_flutter/data/models/user_model.dart';
/// 服务器模型
class ServerModel {
final String label;
final String baseUrl;
bool rememberMe;
String? email;
String? password;
UserModel? user;
ServerModel({
required this.label,
required this.baseUrl,
this.rememberMe = true,
this.email,
this.password,
this.user,
});
ServerModel copyWith({
String? label,
String? baseUrl,
bool? rememberMe,
String? email,
String? password,
UserModel? user,
}) {
return ServerModel(
label: label ?? this.label,
baseUrl: baseUrl ?? this.baseUrl,
rememberMe: rememberMe ?? this.rememberMe,
email: email ?? this.email,
password: password ?? this.password,
user: user ?? this.user,
);
}
factory ServerModel.fromJson(Map<String, dynamic> json) {
return ServerModel(
label: json['label'] as String,
baseUrl: json['baseUrl'] as String,
rememberMe: json['rememberMe'] as bool? ?? true,
email: json['email'] as String?,
password: json['password'] as String?,
user: json['user'] != null
? UserModel.fromJson(json['user'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'label': label,
'baseUrl': baseUrl,
'rememberMe': rememberMe,
'email': email,
'password': password,
'user': user?.toJson(),
};
}
}
+212
View File
@@ -0,0 +1,212 @@
/// 分享模型
class ShareModel {
final String id;
final String name;
final int visited;
final int? downloaded;
final int? price;
final bool unlocked;
final int sourceType;
final ShareOwner? owner;
final DateTime createdAt;
final DateTime? expires;
final bool expired;
final String url;
final int? size;
final SharePermissionSetting? permissionSetting;
final bool? isPrivate;
final String? password;
final bool? shareView;
final String? sourceUri;
final bool? showReadme;
final bool? passwordProtected;
ShareModel({
required this.id,
required this.name,
required this.visited,
this.downloaded,
this.price,
required this.unlocked,
required this.sourceType,
this.owner,
required this.createdAt,
this.expires,
required this.expired,
required this.url,
this.size,
this.permissionSetting,
this.isPrivate,
this.password,
this.shareView,
this.sourceUri,
this.showReadme,
this.passwordProtected,
});
factory ShareModel.fromJson(Map<String, dynamic> json) {
return ShareModel(
id: json['id'] as String,
name: json['name'] as String,
visited: (json['visited'] as num?)?.toInt() ?? 0,
downloaded: (json['downloaded'] as num?)?.toInt(),
price: (json['price'] as num?)?.toInt(),
unlocked: json['unlocked'] as bool? ?? false,
sourceType: (json['source_type'] as num?)?.toInt() ?? 0,
owner: json['owner'] is Map<String, dynamic>
? ShareOwner.fromJson(json['owner'] as Map<String, dynamic>)
: null,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: DateTime.now(),
expires: json['expires'] != null
? DateTime.parse(json['expires'] as String)
: null,
expired: json['expired'] as bool? ?? false,
url: json['url'] as String? ?? '',
size: (json['size'] as num?)?.toInt(),
permissionSetting: json['permission_setting'] is Map<String, dynamic>
? SharePermissionSetting.fromJson(json['permission_setting'] as Map<String, dynamic>)
: null,
isPrivate: json['is_private'] as bool?,
password: json['password'] as String?,
shareView: json['share_view'] as bool?,
sourceUri: json['source_uri'] as String?,
showReadme: json['show_readme'] as bool?,
passwordProtected: json['password_protected'] as bool?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'visited': visited,
'downloaded': downloaded,
'price': price,
'unlocked': unlocked,
'source_type': sourceType,
'owner': owner?.toJson(),
'created_at': createdAt.toIso8601String(),
'expires': expires?.toIso8601String(),
'expired': expired,
'url': url,
'size': size,
'permission_setting': permissionSetting?.toJson(),
'is_private': isPrivate,
'password': password,
'share_view': shareView,
'source_uri': sourceUri,
'show_readme': showReadme,
'password_protected': passwordProtected,
};
}
bool get isFolder => sourceType == 1;
bool get isFile => sourceType == 0;
}
/// 分享所有者信息
class ShareOwner {
final String id;
final String? email;
final String nickname;
final DateTime createdAt;
final ShareOwnerGroup? group;
final String? shareLinksInProfile;
ShareOwner({
required this.id,
this.email,
required this.nickname,
required this.createdAt,
this.group,
this.shareLinksInProfile,
});
factory ShareOwner.fromJson(Map<String, dynamic> json) {
return ShareOwner(
id: json['id'] as String,
email: json['email'] as String?,
nickname: json['nickname'] as String,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: DateTime.now(),
group: json['group'] is Map<String, dynamic>
? ShareOwnerGroup.fromJson(json['group'] as Map<String, dynamic>)
: null,
shareLinksInProfile: json['share_links_in_profile'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'nickname': nickname,
'created_at': createdAt.toIso8601String(),
'group': group?.toJson(),
'share_links_in_profile': shareLinksInProfile,
};
}
}
/// 分享所有者所属组
class ShareOwnerGroup {
final String id;
final String name;
ShareOwnerGroup({required this.id, required this.name});
factory ShareOwnerGroup.fromJson(Map<String, dynamic> json) {
return ShareOwnerGroup(
id: json['id'] as String,
name: json['name'] as String,
);
}
Map<String, dynamic> toJson() => {'id': id, 'name': name};
}
/// 权限设置
class SharePermissionSetting {
final String? sameGroup;
final String? other;
final String? anonymous;
final String? everyone;
final Map<String, String>? groupExplicit;
final Map<String, String>? userExplicit;
SharePermissionSetting({
this.sameGroup,
this.other,
this.anonymous,
this.everyone,
this.groupExplicit,
this.userExplicit,
});
factory SharePermissionSetting.fromJson(Map<String, dynamic> json) {
return SharePermissionSetting(
sameGroup: json['same_group'] as String?,
other: json['other'] as String?,
anonymous: json['anonymous'] as String?,
everyone: json['everyone'] as String?,
groupExplicit: (json['group_explicit'] as Map<String, dynamic>?)
?.cast<String, String>(),
userExplicit: (json['user_explicit'] as Map<String, dynamic>?)
?.cast<String, String>(),
);
}
Map<String, dynamic> toJson() {
return {
'same_group': sameGroup,
'other': other,
'anonymous': anonymous,
'everyone': everyone,
'group_explicit': groupExplicit,
'user_explicit': userExplicit,
};
}
}
+255
View File
@@ -0,0 +1,255 @@
import 'dart:io';
/// 上传状态
enum UploadStatus {
waiting, // 等待中
uploading, // 上传中
completed, // 已完成
paused, // 已暂停
failed, // 失败
cancelled, // 已取消
}
/// 上传会话模型(从 API 返回的上传会话信息)
class UploadSessionModel {
final String sessionId;
final String? uploadId;
final int chunkSize;
final int expires;
final List<String>? uploadUrls;
final String? credential;
final String? completeUrl;
final StoragePolicyModel storagePolicy;
final String? mimeType;
final String? uploadPolicy;
final String? callbackSecret;
UploadSessionModel({
required this.sessionId,
this.uploadId,
required this.chunkSize,
required this.expires,
this.uploadUrls,
this.credential,
this.completeUrl,
required this.storagePolicy,
this.mimeType,
this.uploadPolicy,
this.callbackSecret,
});
factory UploadSessionModel.fromJson(Map<String, dynamic> json) {
return UploadSessionModel(
sessionId: json['session_id'] as String,
uploadId: json['upload_id'] as String?,
chunkSize: json['chunk_size'] as int,
expires: json['expires'] as int,
uploadUrls: json['upload_urls'] != null
? (json['upload_urls'] as List).map((e) => e as String).toList()
: null,
credential: json['credential'] as String?,
completeUrl: json['completeURL'] as String?,
storagePolicy: StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>),
mimeType: json['mime_type'] as String?,
uploadPolicy: json['upload_policy'] as String?,
callbackSecret: json['callback_secret'] as String?,
);
}
/// 是否支持分片上传
bool get isMultipartEnabled => chunkSize > 0;
/// 是否使用中继上传(上传到 Cloudreve 服务器)
bool get isRelayUpload => storagePolicy.relay == true || storagePolicy.type == 'local';
}
/// 存储策略模型
class StoragePolicyModel {
final String id;
final String name;
final String type;
final int maxSize;
final bool? relay;
StoragePolicyModel({
required this.id,
required this.name,
required this.type,
required this.maxSize,
this.relay,
});
factory StoragePolicyModel.fromJson(Map<String, dynamic> json) {
return StoragePolicyModel(
id: json['id'] as String,
name: json['name'] as String,
type: json['type'] as String,
maxSize: json['max_size'] as int,
relay: json['relay'] as bool?,
);
}
}
/// 上传任务模型
class UploadTaskModel {
final String id;
final File file;
final String fileName;
final int fileSize;
final String targetPath; // 目标路径,例如 cloudreve://my/subfolder
final DateTime createdAt;
DateTime? completedAt;
final UploadStatus status;
final int uploadedBytes;
final double progress;
final int uploadedChunks; // 已上传的分片数量
final int totalChunks; // 总分片数量
final String? errorMessage;
final UploadSessionModel? session; // 上传会话信息
final int speed; // 上传速度,字节/秒
UploadTaskModel({
required this.id,
required this.file,
required this.fileName,
required this.fileSize,
required this.targetPath,
DateTime? createdAt,
this.completedAt,
UploadStatus? status,
this.uploadedBytes = 0,
this.progress = 0,
this.uploadedChunks = 0,
this.totalChunks = 1,
this.errorMessage,
this.session,
this.speed = 0,
}) : createdAt = createdAt ?? DateTime.now(),
status = status ?? UploadStatus.waiting;
/// 计算总分片数
int calculateTotalChunks(int chunkSize) {
if (chunkSize <= 0) return 1;
return (fileSize / chunkSize).ceil();
}
/// 获取状态文本
String get statusText {
switch (status) {
case UploadStatus.waiting:
return '等待中';
case UploadStatus.uploading:
return '上传中...';
case UploadStatus.completed:
return '上传完成';
case UploadStatus.paused:
return '已暂停';
case UploadStatus.failed:
return '上传失败';
case UploadStatus.cancelled:
return '已取消';
}
}
/// 获取进度文本
String get progressText {
if (status == UploadStatus.completed) {
return '100%';
}
return '${(progress * 100).toStringAsFixed(1)}%';
}
/// 获取可读的文件大小
String get readableFileSize {
if (fileSize < 1024) {
return '$fileSize B';
} else if (fileSize < 1024 * 1024) {
return '${(fileSize / 1024).toStringAsFixed(1)} KB';
} else if (fileSize < 1024 * 1024 * 1024) {
return '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(fileSize / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
/// 获取可读的上传速度
String get speedText {
if (speed <= 0) return '';
if (speed < 1024) return '$speed B/s';
if (speed < 1024 * 1024) return '${(speed / 1024).toStringAsFixed(1)} KB/s';
return '${(speed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
}
/// 克隆任务
UploadTaskModel copyWith({
UploadStatus? status,
int? uploadedBytes,
double? progress,
int? uploadedChunks,
int? totalChunks,
String? errorMessage,
UploadSessionModel? session,
DateTime? completedAt,
int? speed,
}) {
return UploadTaskModel(
id: id,
file: file,
fileName: fileName,
fileSize: fileSize,
targetPath: targetPath,
createdAt: createdAt,
completedAt: completedAt ?? this.completedAt,
status: status ?? this.status,
uploadedBytes: uploadedBytes ?? this.uploadedBytes,
progress: progress ?? this.progress,
uploadedChunks: uploadedChunks ?? this.uploadedChunks,
totalChunks: totalChunks ?? this.totalChunks,
errorMessage: errorMessage ?? this.errorMessage,
session: session ?? this.session,
speed: speed ?? this.speed,
);
}
/// 转换为 JSON(用于持久化)
Map<String, dynamic> toJson() {
return {
'id': id,
'file_path': file.path,
'file_name': fileName,
'file_size': fileSize,
'target_path': targetPath,
'created_at': createdAt.toIso8601String(),
'completed_at': completedAt?.toIso8601String(),
'status': status.index,
'uploaded_bytes': uploadedBytes,
'progress': progress,
'uploaded_chunks': uploadedChunks,
'total_chunks': totalChunks,
'error_message': errorMessage,
'speed': speed,
};
}
/// 从 JSON 创建(用于持久化恢复)
factory UploadTaskModel.fromJson(Map<String, dynamic> json) {
return UploadTaskModel(
id: json['id'] as String,
file: File(json['file_path'] as String),
fileName: json['file_name'] as String,
fileSize: json['file_size'] as int,
targetPath: json['target_path'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
completedAt: json['completed_at'] != null
? DateTime.parse(json['completed_at'] as String)
: null,
status: UploadStatus.values[json['status'] as int],
uploadedBytes: json['uploaded_bytes'] as int,
progress: json['progress'] as double,
uploadedChunks: json['uploaded_chunks'] as int,
totalChunks: json['total_chunks'] as int,
errorMessage: json['error_message'] as String?,
speed: json['speed'] as int? ?? 0,
);
}
}
+224
View File
@@ -0,0 +1,224 @@
/// 用户模型
class UserModel {
final String id;
final String? email;
final String nickname;
final String? avatar;
final DateTime createdAt;
final String? preferredTheme;
final String? language;
final bool? anonymous;
final GroupModel? group;
final List<PinedFileModel>? pined;
// Token 信息
final TokenModel? token;
UserModel({
required this.id,
this.email,
required this.nickname,
this.avatar,
required this.createdAt,
this.preferredTheme,
this.language,
this.anonymous,
this.group,
this.pined,
this.token,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
email: json['email'] as String?,
nickname: json['nickname'] as String,
avatar: json['avatar'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
preferredTheme: json['preferred_theme'] as String?,
language: json['language'] as String?,
anonymous: json['anonymous'] as bool?,
group: json['group'] != null
? GroupModel.fromJson(json['group'] as Map<String, dynamic>)
: null,
pined: json['pined'] != null
? (json['pined'] as List)
.map((e) => PinedFileModel.fromJson(e as Map<String, dynamic>))
.toList()
: null,
token: json['token'] != null
? TokenModel.fromJson(json['token'] as Map<String, dynamic>)
: null,
);
}
UserModel copyWith({
String? id,
String? email,
String? nickname,
String? avatar,
DateTime? createdAt,
String? preferredTheme,
String? language,
bool? anonymous,
GroupModel? group,
List<PinedFileModel>? pined,
TokenModel? token,
}) {
return UserModel(
id: id ?? this.id,
email: email ?? this.email,
nickname: nickname ?? this.nickname,
avatar: avatar ?? this.avatar,
createdAt: createdAt ?? this.createdAt,
preferredTheme: preferredTheme ?? this.preferredTheme,
language: language ?? this.language,
anonymous: anonymous ?? this.anonymous,
group: group ?? this.group,
pined: pined ?? this.pined,
token: token ?? this.token,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'nickname': nickname,
'avatar': avatar,
'created_at': createdAt.toIso8601String(),
'preferred_theme': preferredTheme,
'language': language,
'anonymous': anonymous,
'group': group?.toJson(),
'pined': pined?.map((e) => e.toJson()).toList(),
'token': token?.toJson(),
};
}
}
/// 用户组模型
class GroupModel {
final String id;
final String name;
final String? permission;
final int? directLinkBatchSize;
final int? trashRetention;
GroupModel({
required this.id,
required this.name,
this.permission,
this.directLinkBatchSize,
this.trashRetention,
});
factory GroupModel.fromJson(Map<String, dynamic> json) {
return GroupModel(
id: json['id'] as String,
name: json['name'] as String,
permission: json['permission'] as String?,
directLinkBatchSize: json['direct_link_batch_size'] as int?,
trashRetention: json['trash_retention'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'permission': permission,
'direct_link_batch_size': directLinkBatchSize,
'trash_retention': trashRetention,
};
}
}
/// 固定文件模型
class PinedFileModel {
final String uri;
final String? name;
PinedFileModel({required this.uri, this.name});
factory PinedFileModel.fromJson(Map<String, dynamic> json) {
return PinedFileModel(
uri: json['uri'] as String,
name: json['name'] as String?,
);
}
Map<String, dynamic> toJson() {
return {'uri': uri, 'name': name};
}
}
/// Token模型
class TokenModel {
final String accessToken;
final String refreshToken;
final DateTime accessExpires;
final DateTime refreshExpires;
TokenModel({
required this.accessToken,
required this.refreshToken,
required this.accessExpires,
required this.refreshExpires,
});
factory TokenModel.fromJson(Map<String, dynamic> json) {
// 支持两种格式:直接是 token 数据或嵌套在 data 中
Map<String, dynamic> data;
if (json['data'] != null) {
data = json['data'] as Map<String, dynamic>;
} else {
data = json;
}
return TokenModel(
accessToken: data['access_token'] as String,
refreshToken: data['refresh_token'] as String,
accessExpires: DateTime.parse(data['access_expires'] as String),
refreshExpires: DateTime.parse(data['refresh_expires'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'refresh_token': refreshToken,
'access_expires': accessExpires.toIso8601String(),
'refresh_expires': refreshExpires.toIso8601String(),
};
}
/// 检查 access token 是否过期
bool get isAccessTokenExpired => DateTime.now().isAfter(accessExpires);
/// 检查 refresh token 是否过期
bool get isRefreshTokenExpired => DateTime.now().isAfter(refreshExpires);
}
/// 用户容量模型
class CapacityModel {
final int total;
final int used;
double get usagePercentage => total > 0 ? (used / total) * 100 : 0;
int get remaining => total - used;
CapacityModel({required this.total, required this.used});
factory CapacityModel.fromJson(Map<String, dynamic> json) {
return CapacityModel(
total: json['total'] as int,
used: json['used'] as int,
);
}
Map<String, dynamic> toJson() {
return {'total': total, 'used': used};
}
}
+346
View File
@@ -0,0 +1,346 @@
/// 用户设置模型 - 对应 GET /user/setting 响应
class UserSettingModel {
final DateTime? groupExpires;
final List<OpenIdProvider> openId;
final bool versionRetentionEnabled;
final List<String>? versionRetentionExt;
final int versionRetentionMax;
final bool passwordless;
final bool twoFaEnabled;
final List<PasskeyModel> passkeys;
final List<LoginActivity> loginActivity;
final List<StoragePack> storagePacks;
final int credit;
final bool disableViewSync;
final String shareLinksInProfile;
final List<OAuthGrant> oauthGrants;
UserSettingModel({
this.groupExpires,
this.openId = const [],
this.versionRetentionEnabled = false,
this.versionRetentionExt,
this.versionRetentionMax = 0,
this.passwordless = false,
this.twoFaEnabled = false,
this.passkeys = const [],
this.loginActivity = const [],
this.storagePacks = const [],
this.credit = 0,
this.disableViewSync = false,
this.shareLinksInProfile = '',
this.oauthGrants = const [],
});
factory UserSettingModel.fromJson(Map<String, dynamic> json) {
return UserSettingModel(
groupExpires: json['group_expires'] != null
? DateTime.parse(json['group_expires'] as String)
: null,
openId: (json['open_id'] as List?)
?.map((e) => OpenIdProvider.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
versionRetentionEnabled: json['version_retention_enabled'] as bool? ?? false,
versionRetentionExt: (json['version_retention_ext'] as List?)
?.map((e) => e as String)
.toList(),
versionRetentionMax: json['version_retention_max'] as int? ?? 0,
passwordless: json['passwordless'] as bool? ?? false,
twoFaEnabled: json['two_fa_enabled'] as bool? ?? false,
passkeys: (json['passkeys'] as List?)
?.map((e) => PasskeyModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
loginActivity: (json['login_activity'] as List?)
?.map((e) => LoginActivity.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
storagePacks: (json['storage_packs'] as List?)
?.map((e) => StoragePack.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
credit: json['credit'] as int? ?? 0,
disableViewSync: json['disable_view_sync'] as bool? ?? false,
shareLinksInProfile: json['share_links_in_profile'] as String? ?? '',
oauthGrants: (json['oauth_grants'] as List?)
?.map((e) => OAuthGrant.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
UserSettingModel copyWith({
DateTime? groupExpires,
List<OpenIdProvider>? openId,
bool? versionRetentionEnabled,
List<String>? versionRetentionExt,
int? versionRetentionMax,
bool? passwordless,
bool? twoFaEnabled,
List<PasskeyModel>? passkeys,
List<LoginActivity>? loginActivity,
List<StoragePack>? storagePacks,
int? credit,
bool? disableViewSync,
String? shareLinksInProfile,
List<OAuthGrant>? oauthGrants,
}) {
return UserSettingModel(
groupExpires: groupExpires ?? this.groupExpires,
openId: openId ?? this.openId,
versionRetentionEnabled: versionRetentionEnabled ?? this.versionRetentionEnabled,
versionRetentionExt: versionRetentionExt ?? this.versionRetentionExt,
versionRetentionMax: versionRetentionMax ?? this.versionRetentionMax,
passwordless: passwordless ?? this.passwordless,
twoFaEnabled: twoFaEnabled ?? this.twoFaEnabled,
passkeys: passkeys ?? this.passkeys,
loginActivity: loginActivity ?? this.loginActivity,
storagePacks: storagePacks ?? this.storagePacks,
credit: credit ?? this.credit,
disableViewSync: disableViewSync ?? this.disableViewSync,
shareLinksInProfile: shareLinksInProfile ?? this.shareLinksInProfile,
oauthGrants: oauthGrants ?? this.oauthGrants,
);
}
}
/// 已关联的外部身份提供商
class OpenIdProvider {
final int provider;
final DateTime linkedAt;
OpenIdProvider({required this.provider, required this.linkedAt});
factory OpenIdProvider.fromJson(Map<String, dynamic> json) {
return OpenIdProvider(
provider: json['provider'] as int,
linkedAt: DateTime.parse(json['linked_at'] as String),
);
}
String get providerName {
switch (provider) {
case 0:
return 'Logto';
case 1:
return 'QQ';
case 2:
return 'OIDC';
default:
return 'Unknown';
}
}
}
/// Passkey 模型
class PasskeyModel {
final String id;
final String name;
final DateTime? usedAt;
final DateTime createdAt;
PasskeyModel({
required this.id,
required this.name,
this.usedAt,
required this.createdAt,
});
factory PasskeyModel.fromJson(Map<String, dynamic> json) {
return PasskeyModel(
id: json['id'] as String,
name: json['name'] as String,
usedAt: json['used_at'] != null
? DateTime.parse(json['used_at'] as String)
: null,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
}
/// 登录活动记录
class LoginActivity {
final DateTime createdAt;
final String ip;
final String browser;
final String device;
final String os;
final String loginWith;
final int? openIdProvider;
final bool success;
final bool webdav;
LoginActivity({
required this.createdAt,
required this.ip,
required this.browser,
required this.device,
required this.os,
required this.loginWith,
this.openIdProvider,
required this.success,
required this.webdav,
});
factory LoginActivity.fromJson(Map<String, dynamic> json) {
return LoginActivity(
createdAt: DateTime.parse(json['created_at'] as String),
ip: json['ip'] as String,
browser: json['browser'] as String,
device: json['device'] as String,
os: json['os'] as String,
loginWith: json['login_with'] as String? ?? '',
openIdProvider: json['open_id_provider'] as int?,
success: json['success'] as bool? ?? false,
webdav: json['webdav'] as bool? ?? false,
);
}
String get loginMethodName {
if (webdav) return 'WebDAV';
switch (loginWith) {
case 'passkey':
return 'Passkey';
case 'openid':
return '第三方登录';
default:
return '密码';
}
}
}
/// 存储包
class StoragePack {
final String name;
final DateTime activeSince;
final DateTime? expireAt;
final int size;
StoragePack({
required this.name,
required this.activeSince,
this.expireAt,
required this.size,
});
factory StoragePack.fromJson(Map<String, dynamic> json) {
return StoragePack(
name: json['name'] as String,
activeSince: DateTime.parse(json['active_since'] as String),
expireAt: json['expire_at'] != null
? DateTime.parse(json['expire_at'] as String)
: null,
size: json['size'] as int,
);
}
bool get isExpired => expireAt != null && DateTime.now().isAfter(expireAt!);
}
/// OAuth 授权应用
class OAuthGrant {
final String clientId;
final String clientName;
final String? clientLogo;
final List<String> scopes;
final DateTime? lastUsedAt;
OAuthGrant({
required this.clientId,
required this.clientName,
this.clientLogo,
this.scopes = const [],
this.lastUsedAt,
});
factory OAuthGrant.fromJson(Map<String, dynamic> json) {
return OAuthGrant(
clientId: json['client_id'] as String,
clientName: json['client_name'] as String,
clientLogo: json['client_logo'] as String?,
scopes: (json['scopes'] as List?)?.map((e) => e as String).toList() ?? [],
lastUsedAt: json['last_used_at'] != null
? DateTime.parse(json['last_used_at'] as String)
: null,
);
}
}
/// 用户容量模型(扩展版,含存储包)
class UserCapacityModel {
final int total;
final int used;
final int storagePackTotal;
UserCapacityModel({
required this.total,
required this.used,
this.storagePackTotal = 0,
});
factory UserCapacityModel.fromJson(Map<String, dynamic> json) {
return UserCapacityModel(
total: json['total'] as int,
used: json['used'] as int,
storagePackTotal: json['storage_pack_total'] as int? ?? 0,
);
}
double get usagePercentage => total > 0 ? (used / total) * 100 : 0;
int get remaining => total - used;
}
/// 积分变动记录
class CreditChange {
final DateTime changedAt;
final int diff;
final String reason;
CreditChange({
required this.changedAt,
required this.diff,
required this.reason,
});
factory CreditChange.fromJson(Map<String, dynamic> json) {
return CreditChange(
changedAt: DateTime.parse(json['changed_at'] as String),
diff: json['diff'] as int,
reason: json['reason'] as String,
);
}
String get reasonLabel {
switch (reason) {
case 'share_purchased':
return '分享被购买';
case 'pay':
return '支付';
case 'adjust':
return '管理员调整';
default:
return reason;
}
}
}
/// 积分变动列表响应
class CreditChangeList {
final List<CreditChange> changes;
final String? nextToken;
CreditChangeList({required this.changes, this.nextToken});
factory CreditChangeList.fromJson(Map<String, dynamic> json) {
return CreditChangeList(
changes: (json['changes'] as List?)
?.map((e) => CreditChange.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
nextToken: (json['pagination'] as Map<String, dynamic>?)?['next_token'] as String?,
);
}
bool get hasMore => nextToken != null && nextToken!.isNotEmpty;
}