726 lines
22 KiB
Dart
726 lines
22 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_riverpod/legacy.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
import '../api/guangya_api.dart';
|
||
import '../core/storage/storage_manager.dart';
|
||
import '../models/cloud_file.dart';
|
||
|
||
enum FileSort { name, size, modifiedAt, createdAt, type }
|
||
|
||
extension FileSortExt on FileSort {
|
||
String get title {
|
||
switch (this) {
|
||
case FileSort.name:
|
||
return '名称';
|
||
case FileSort.size:
|
||
return '大小';
|
||
case FileSort.modifiedAt:
|
||
return '修改时间';
|
||
case FileSort.createdAt:
|
||
return '创建时间';
|
||
case FileSort.type:
|
||
return '类型';
|
||
}
|
||
}
|
||
|
||
int get apiOrderBy {
|
||
switch (this) {
|
||
case FileSort.name:
|
||
return 0;
|
||
case FileSort.size:
|
||
return 1;
|
||
case FileSort.createdAt:
|
||
return 2;
|
||
case FileSort.modifiedAt:
|
||
return 3;
|
||
case FileSort.type:
|
||
return 4;
|
||
}
|
||
}
|
||
}
|
||
|
||
enum SortDirection { ascending, descending }
|
||
|
||
enum WorkspaceSection {
|
||
files('全部', 'folder'),
|
||
recentViewed('最近查看', 'clock'),
|
||
recentRestored('最近转存', 'history'),
|
||
photos('图片', 'image'),
|
||
videos('视频', 'movie'),
|
||
audio('音频', 'music_note'),
|
||
documents('文档', 'description'),
|
||
cloud('云下载', 'cloud_download'),
|
||
shares('我的分享', 'share'),
|
||
recycle('回收站', 'delete'),
|
||
mediaLibrary('光鸭影视', 'movie_filter');
|
||
|
||
final String label;
|
||
final String icon;
|
||
const WorkspaceSection(this.label, this.icon);
|
||
}
|
||
|
||
class FileState {
|
||
final WorkspaceSection section;
|
||
final List<CloudFile> files;
|
||
final List<CloudFile> folderPath;
|
||
final bool isLoading;
|
||
final int currentPage;
|
||
final int pageSize;
|
||
final int totalPages;
|
||
final FileSort serverSort;
|
||
final SortDirection serverSortDirection;
|
||
final String? errorMessage;
|
||
final String? statusMessage;
|
||
final Set<String> selectedIDs;
|
||
final List<CloudFile>? clipboard;
|
||
final bool clipboardIsMove;
|
||
|
||
const FileState({
|
||
this.section = WorkspaceSection.files,
|
||
this.files = const [],
|
||
this.folderPath = const [],
|
||
this.isLoading = false,
|
||
this.currentPage = 0,
|
||
this.pageSize = 50,
|
||
this.totalPages = 1,
|
||
this.serverSort = FileSort.name,
|
||
this.serverSortDirection = SortDirection.ascending,
|
||
this.errorMessage,
|
||
this.statusMessage,
|
||
this.selectedIDs = const {},
|
||
this.clipboard,
|
||
this.clipboardIsMove = false,
|
||
});
|
||
|
||
bool get hasSelection => selectedIDs.isNotEmpty;
|
||
int get selectedCount => selectedIDs.length;
|
||
|
||
FileState copyWith({
|
||
WorkspaceSection? section,
|
||
List<CloudFile>? files,
|
||
List<CloudFile>? folderPath,
|
||
bool? isLoading,
|
||
int? currentPage,
|
||
int? pageSize,
|
||
int? totalPages,
|
||
FileSort? serverSort,
|
||
SortDirection? serverSortDirection,
|
||
String? errorMessage,
|
||
bool clearError = false,
|
||
String? statusMessage,
|
||
bool clearStatus = false,
|
||
Set<String>? selectedIDs,
|
||
List<CloudFile>? clipboard,
|
||
bool clearClipboard = false,
|
||
bool? clipboardIsMove,
|
||
}) {
|
||
return FileState(
|
||
section: section ?? this.section,
|
||
files: files ?? this.files,
|
||
folderPath: folderPath ?? this.folderPath,
|
||
isLoading: isLoading ?? this.isLoading,
|
||
currentPage: currentPage ?? this.currentPage,
|
||
pageSize: pageSize ?? this.pageSize,
|
||
totalPages: totalPages ?? this.totalPages,
|
||
serverSort: serverSort ?? this.serverSort,
|
||
serverSortDirection: serverSortDirection ?? this.serverSortDirection,
|
||
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
||
statusMessage: clearStatus ? null : (statusMessage ?? this.statusMessage),
|
||
selectedIDs: selectedIDs ?? this.selectedIDs,
|
||
clipboard: clearClipboard ? null : (clipboard ?? this.clipboard),
|
||
clipboardIsMove: clipboardIsMove ?? this.clipboardIsMove,
|
||
);
|
||
}
|
||
}
|
||
|
||
class FileNotifier extends StateNotifier<FileState> {
|
||
GuangyaAPI? _api;
|
||
|
||
FileNotifier() : super(const FileState());
|
||
|
||
set api(GuangyaAPI value) => _api = value;
|
||
|
||
String? get _currentParentID =>
|
||
state.folderPath.isNotEmpty ? state.folderPath.last.id : null;
|
||
|
||
void setSection(WorkspaceSection section) {
|
||
final preservePath = section == WorkspaceSection.mediaLibrary;
|
||
state = state.copyWith(
|
||
section: section,
|
||
files: [],
|
||
folderPath: preservePath ? state.folderPath : [],
|
||
currentPage: 0,
|
||
selectedIDs: {},
|
||
);
|
||
loadFiles();
|
||
}
|
||
|
||
Future<void> loadFiles({String? parentID}) async {
|
||
if (_api == null) return;
|
||
final resolvedParentID = parentID ?? _currentParentID;
|
||
final cacheKey =
|
||
'${state.section.name}:${resolvedParentID ?? 'root'}:${state.currentPage}:${state.pageSize}';
|
||
final cached = _readCachedFiles(cacheKey);
|
||
if (cached != null) {
|
||
state = state.copyWith(files: cached, clearError: true);
|
||
return;
|
||
}
|
||
state = state.copyWith(isLoading: true, clearError: true);
|
||
|
||
try {
|
||
final result = await _fetchFiles(resolvedParentID);
|
||
final extracted = _extractFiles(result);
|
||
final totalPages = _extractTotalPages(result, extracted.length);
|
||
state = state.copyWith(files: extracted, totalPages: totalPages);
|
||
await _writeCachedFiles(cacheKey, extracted);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
|
||
List<CloudFile>? _readCachedFiles(String key) {
|
||
final raw = StorageManager.get<dynamic>(StorageKeys.fileListCache);
|
||
if (raw is! Map || raw[key] is! Map) return null;
|
||
final entry = Map<dynamic, dynamic>.from(raw[key] as Map);
|
||
final cachedAt = int.tryParse(entry['cachedAt']?.toString() ?? '');
|
||
final files = entry['files'];
|
||
final ttlMinutes =
|
||
int.tryParse(
|
||
StorageManager.get<String>(StorageKeys.fileCacheTTLMinutes) ?? '3',
|
||
) ??
|
||
3;
|
||
if (cachedAt == null ||
|
||
files is! List ||
|
||
DateTime.now().millisecondsSinceEpoch - cachedAt >
|
||
Duration(minutes: ttlMinutes.clamp(0, 60)).inMilliseconds) {
|
||
return null;
|
||
}
|
||
try {
|
||
return files
|
||
.whereType<Map>()
|
||
.map((value) => CloudFile.fromJson(Map<String, dynamic>.from(value)))
|
||
.toList();
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
Future<void> _writeCachedFiles(String key, List<CloudFile> files) async {
|
||
final raw = StorageManager.get<dynamic>(StorageKeys.fileListCache);
|
||
final cache = raw is Map
|
||
? Map<dynamic, dynamic>.from(raw)
|
||
: <dynamic, dynamic>{};
|
||
cache[key] = {
|
||
'cachedAt': DateTime.now().millisecondsSinceEpoch,
|
||
'files': files.map((file) => file.toJson()).toList(),
|
||
};
|
||
if (cache.length > 80) cache.remove(cache.keys.first);
|
||
await StorageManager.set(StorageKeys.fileListCache, cache);
|
||
}
|
||
|
||
Future<Map<String, dynamic>> _fetchFiles(String? parentID) async {
|
||
switch (state.section) {
|
||
case WorkspaceSection.files:
|
||
return await _api!.fsFiles(
|
||
parentID: parentID,
|
||
page: state.currentPage,
|
||
pageSize: state.pageSize,
|
||
orderBy: state.serverSort.apiOrderBy,
|
||
sortType: state.serverSortDirection == SortDirection.ascending
|
||
? 0
|
||
: 1,
|
||
);
|
||
case WorkspaceSection.recentViewed:
|
||
return await _api!.recentViewed(pageSize: 100);
|
||
case WorkspaceSection.recentRestored:
|
||
return await _api!.recentRestored(pageSize: 100);
|
||
case WorkspaceSection.photos:
|
||
return await _api!.fsFiles(
|
||
parentID: '*',
|
||
orderBy: 3,
|
||
sortType: 1,
|
||
fileTypes: [1],
|
||
resType: 1,
|
||
);
|
||
case WorkspaceSection.videos:
|
||
return await _api!.fsFiles(
|
||
parentID: '*',
|
||
orderBy: 3,
|
||
sortType: 1,
|
||
fileTypes: [2],
|
||
resType: 1,
|
||
);
|
||
case WorkspaceSection.audio:
|
||
return await _api!.fsFiles(
|
||
parentID: '*',
|
||
orderBy: 3,
|
||
sortType: 1,
|
||
fileTypes: [3],
|
||
resType: 1,
|
||
needPlayRecord: true,
|
||
);
|
||
case WorkspaceSection.documents:
|
||
return await _api!.fsFiles(
|
||
parentID: '*',
|
||
orderBy: 3,
|
||
sortType: 1,
|
||
fileTypes: [4],
|
||
resType: 1,
|
||
);
|
||
case WorkspaceSection.cloud:
|
||
return await _api!.cloudTaskList();
|
||
case WorkspaceSection.shares:
|
||
return await _api!.shareUserList();
|
||
case WorkspaceSection.recycle:
|
||
return await _api!.fsFiles(orderBy: 10, dirType: 4);
|
||
case WorkspaceSection.mediaLibrary:
|
||
return {
|
||
'code': 0,
|
||
'data': {'list': <dynamic>[], 'total': 0},
|
||
};
|
||
}
|
||
}
|
||
|
||
Future<void> navigateToFolder(CloudFile folder) async {
|
||
final newPath = [...state.folderPath, folder];
|
||
state = state.copyWith(
|
||
folderPath: newPath,
|
||
currentPage: 0,
|
||
selectedIDs: {},
|
||
);
|
||
await loadFiles(parentID: folder.id);
|
||
}
|
||
|
||
Future<void> navigateBack() async {
|
||
if (state.folderPath.isEmpty) return;
|
||
final newPath = List<CloudFile>.from(state.folderPath)..removeLast();
|
||
state = state.copyWith(
|
||
folderPath: newPath,
|
||
currentPage: 0,
|
||
selectedIDs: {},
|
||
);
|
||
final parentID = newPath.isNotEmpty ? newPath.last.id : null;
|
||
await loadFiles(parentID: parentID);
|
||
}
|
||
|
||
void navigateToPathIndex(int index) {
|
||
if (index < 0) {
|
||
state = state.copyWith(
|
||
folderPath: const [],
|
||
currentPage: 0,
|
||
selectedIDs: {},
|
||
);
|
||
loadFiles(parentID: null);
|
||
return;
|
||
}
|
||
if (index >= state.folderPath.length) return;
|
||
final newPath = state.folderPath.sublist(0, index + 1);
|
||
state = state.copyWith(
|
||
folderPath: newPath,
|
||
currentPage: 0,
|
||
selectedIDs: {},
|
||
);
|
||
final parentID = index >= 0 ? newPath[index].id : null;
|
||
loadFiles(parentID: parentID);
|
||
}
|
||
|
||
void toggleSelection(String id) {
|
||
final newSelected = Set<String>.from(state.selectedIDs);
|
||
if (newSelected.contains(id)) {
|
||
newSelected.remove(id);
|
||
} else {
|
||
newSelected.add(id);
|
||
}
|
||
state = state.copyWith(selectedIDs: newSelected);
|
||
}
|
||
|
||
void selectAll() {
|
||
if (state.selectedIDs.length == state.files.length) {
|
||
state = state.copyWith(selectedIDs: {});
|
||
} else {
|
||
state = state.copyWith(selectedIDs: state.files.map((f) => f.id).toSet());
|
||
}
|
||
}
|
||
|
||
void clearSelection() {
|
||
state = state.copyWith(selectedIDs: {});
|
||
}
|
||
|
||
void setSort(FileSort sort) {
|
||
SortDirection direction;
|
||
if (state.serverSort == sort) {
|
||
direction = state.serverSortDirection == SortDirection.ascending
|
||
? SortDirection.descending
|
||
: SortDirection.ascending;
|
||
} else {
|
||
direction = SortDirection.ascending;
|
||
}
|
||
state = state.copyWith(
|
||
serverSort: sort,
|
||
serverSortDirection: direction,
|
||
currentPage: 0,
|
||
);
|
||
loadFiles(parentID: _currentParentID);
|
||
}
|
||
|
||
void nextPage() {
|
||
if (state.currentPage < state.totalPages - 1) {
|
||
state = state.copyWith(currentPage: state.currentPage + 1);
|
||
loadFiles(parentID: _currentParentID);
|
||
}
|
||
}
|
||
|
||
void prevPage() {
|
||
if (state.currentPage > 0) {
|
||
state = state.copyWith(currentPage: state.currentPage - 1);
|
||
loadFiles(parentID: _currentParentID);
|
||
}
|
||
}
|
||
|
||
void setPageSize(int size) {
|
||
state = state.copyWith(pageSize: size, currentPage: 0);
|
||
loadFiles(parentID: _currentParentID);
|
||
}
|
||
|
||
// ── File operations ─────────────────────────────────────────────
|
||
|
||
Future<void> createFolder(String name) async {
|
||
if (_api == null) return;
|
||
state = state.copyWith(statusMessage: '正在创建文件夹…');
|
||
try {
|
||
await _api!.fsCreateDir(name, parentID: _currentParentID);
|
||
state = state.copyWith(statusMessage: '文件夹已创建');
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString(), clearStatus: true);
|
||
}
|
||
}
|
||
|
||
Future<void> renameFile(CloudFile file, String newName) async {
|
||
if (_api == null) return;
|
||
try {
|
||
await _api!.fsRename(file.id, newName);
|
||
state = state.copyWith(statusMessage: '重命名成功');
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> deleteFiles(List<CloudFile> files) async {
|
||
if (_api == null) return;
|
||
state = state.copyWith(statusMessage: '正在删除…');
|
||
try {
|
||
await _api!.fsDelete(files.map((f) => f.id).toList());
|
||
state = state.copyWith(
|
||
statusMessage: '已删除 ${files.length} 个项目',
|
||
selectedIDs: {},
|
||
);
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> recycleFiles(List<CloudFile> files) async {
|
||
if (_api == null) return;
|
||
try {
|
||
await _api!.fsRecycle(files.map((f) => f.id).toList());
|
||
state = state.copyWith(statusMessage: '已移入回收站', selectedIDs: {});
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> clearRecycleBin() async {
|
||
if (_api == null) return;
|
||
try {
|
||
await _api!.fsClearRecycleBin();
|
||
state = state.copyWith(statusMessage: '回收站已清空');
|
||
await loadFiles();
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
void copyToClipboard(List<CloudFile> files) {
|
||
state = state.copyWith(clipboard: files, clipboardIsMove: false);
|
||
}
|
||
|
||
void cutToClipboard(List<CloudFile> files) {
|
||
state = state.copyWith(clipboard: files, clipboardIsMove: true);
|
||
}
|
||
|
||
Future<void> pasteFromClipboard() async {
|
||
if (_api == null || state.clipboard == null) return;
|
||
state = state.copyWith(
|
||
statusMessage: state.clipboardIsMove ? '正在移动…' : '正在复制…',
|
||
);
|
||
try {
|
||
final ids = state.clipboard!.map((f) => f.id).toList();
|
||
if (state.clipboardIsMove) {
|
||
await _api!.fsMove(ids, parentID: _currentParentID);
|
||
} else {
|
||
await _api!.fsCopy(ids, parentID: _currentParentID);
|
||
}
|
||
state = state.copyWith(statusMessage: '操作完成');
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
void clearClipboard() {
|
||
state = state.copyWith(clearClipboard: true);
|
||
}
|
||
|
||
Future<void> downloadFile(CloudFile file) async {
|
||
if (_api == null) return;
|
||
try {
|
||
state = state.copyWith(statusMessage: '正在准备外部打开…');
|
||
final url = await _resolveOpenUrl(file);
|
||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||
throw Exception('无法调用系统默认应用打开链接');
|
||
}
|
||
state = state.copyWith(statusMessage: '已交给系统默认应用');
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> createShare(CloudFile file) async {
|
||
if (_api == null) return;
|
||
try {
|
||
state = state.copyWith(statusMessage: '正在创建分享…');
|
||
final result = await _api!.shareCreate([file.id], title: file.name);
|
||
final link = _findStringDeep(result, const [
|
||
'url',
|
||
'shareUrl',
|
||
'share_url',
|
||
'link',
|
||
]);
|
||
if (link != null) {
|
||
await Clipboard.setData(ClipboardData(text: link));
|
||
state = state.copyWith(statusMessage: '分享链接已复制');
|
||
} else {
|
||
state = state.copyWith(statusMessage: '分享已创建');
|
||
}
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> restoreFiles(List<CloudFile> files) async {
|
||
if (_api == null || files.isEmpty) return;
|
||
try {
|
||
state = state.copyWith(statusMessage: '正在恢复 ${files.length} 个项目…');
|
||
await _api!.fsRecycle(files.map((file) => file.id).toList());
|
||
state = state.copyWith(statusMessage: '已恢复 ${files.length} 个项目');
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<Uri> _resolveOpenUrl(CloudFile file) async {
|
||
String? url;
|
||
if (file.isVideo) {
|
||
final detail = await _api!.fsDetail(file.id);
|
||
final gcid = file.gcid ?? _findStringDeep(detail, const ['gcid', 'gcId']);
|
||
if (gcid != null && gcid.isNotEmpty) {
|
||
final videoResult = await _api!.vodDownloadURL(file.id, gcid);
|
||
url = _findStringDeep(videoResult, const [
|
||
'signedURL',
|
||
'signedUrl',
|
||
'url',
|
||
'downloadUrl',
|
||
'download_url',
|
||
'dlink',
|
||
]);
|
||
}
|
||
}
|
||
if (url == null) {
|
||
final result = await _api!.downloadURL(file.id);
|
||
url = _findStringDeep(result, const [
|
||
'url',
|
||
'downloadUrl',
|
||
'download_url',
|
||
'dlink',
|
||
]);
|
||
}
|
||
final uri = url == null ? null : Uri.tryParse(url);
|
||
if (uri == null || !uri.hasScheme) throw Exception('响应缺少可打开的签名链接');
|
||
return uri;
|
||
}
|
||
|
||
Future<void> uploadLocalFiles(List<File> files, {String? parentID}) async {
|
||
if (_api == null || files.isEmpty) return;
|
||
final targetParentID = parentID ?? _currentParentID;
|
||
state = state.copyWith(statusMessage: '正在上传 ${files.length} 个文件…');
|
||
var completed = 0;
|
||
try {
|
||
for (final file in files) {
|
||
if (!await file.exists()) continue;
|
||
state = state.copyWith(
|
||
statusMessage:
|
||
'正在上传 ${completed + 1}/${files.length}:${file.uri.pathSegments.last}',
|
||
);
|
||
await _api!.fileUpload(file, parentID: targetParentID);
|
||
completed += 1;
|
||
}
|
||
state = state.copyWith(statusMessage: '已上传 $completed 个文件');
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
Future<void> moveFilesTo(List<CloudFile> files, {String? parentID}) async {
|
||
if (_api == null || files.isEmpty) return;
|
||
state = state.copyWith(statusMessage: '正在移动 ${files.length} 个项目…');
|
||
try {
|
||
await _api!.fsMove(
|
||
files.map((file) => file.id).toList(),
|
||
parentID: parentID,
|
||
);
|
||
state = state.copyWith(statusMessage: '移动完成', selectedIDs: {});
|
||
await loadFiles(parentID: _currentParentID);
|
||
} catch (e) {
|
||
state = state.copyWith(errorMessage: e.toString());
|
||
}
|
||
}
|
||
|
||
void clearError() {
|
||
state = state.copyWith(clearError: true);
|
||
}
|
||
|
||
void clearStatus() {
|
||
state = state.copyWith(clearStatus: true);
|
||
}
|
||
|
||
// ── Helpers ─────────────────────────────────────────────────────
|
||
|
||
List<CloudFile> _extractFiles(Map<String, dynamic> json) {
|
||
final result = <CloudFile>[];
|
||
final seen = <String>{};
|
||
|
||
void visit(dynamic value) {
|
||
if (value is Map) {
|
||
final map = Map<String, dynamic>.from(value);
|
||
try {
|
||
final file = CloudFile.fromJson(map);
|
||
if (seen.add(file.id)) result.add(file);
|
||
} catch (_) {}
|
||
for (final v in map.values) {
|
||
visit(v);
|
||
}
|
||
} else if (value is List) {
|
||
for (final v in value) {
|
||
visit(v);
|
||
}
|
||
}
|
||
}
|
||
|
||
final preferred = _findArrayDeep(json, const [
|
||
'list',
|
||
'files',
|
||
'fileList',
|
||
'file_list',
|
||
'items',
|
||
'records',
|
||
'rows',
|
||
'dataList',
|
||
'resList',
|
||
'resourceList',
|
||
]);
|
||
if (preferred != null) {
|
||
visit(preferred);
|
||
return result;
|
||
}
|
||
|
||
visit(json);
|
||
return result;
|
||
}
|
||
|
||
int _extractTotalPages(Map<String, dynamic> json, int itemCount) {
|
||
final explicitPages = _extractInt(json, [
|
||
'totalPages',
|
||
'pages',
|
||
'pageCount',
|
||
]);
|
||
if (explicitPages != null && explicitPages > 0) return explicitPages;
|
||
final totalItems = _extractInt(json, ['total', 'totalCount', 'count']);
|
||
if (totalItems == null || totalItems <= 0) {
|
||
return itemCount < state.pageSize ? 1 : state.currentPage + 2;
|
||
}
|
||
return (totalItems / state.pageSize).ceil().clamp(1, 1 << 31).toInt();
|
||
}
|
||
|
||
static List<dynamic>? _findArrayDeep(
|
||
Map<String, dynamic> json,
|
||
List<String> keys,
|
||
) {
|
||
for (final key in keys) {
|
||
final v = json[key];
|
||
if (v is List) return v;
|
||
}
|
||
const preferredKeys = ['data', 'result', 'payload'];
|
||
for (final key in preferredKeys) {
|
||
final v = json[key];
|
||
if (v is Map) {
|
||
final found = _findArrayDeep(Map<String, dynamic>.from(v), keys);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
for (final entry in json.entries) {
|
||
if (preferredKeys.contains(entry.key)) continue;
|
||
final v = entry.value;
|
||
if (v is Map) {
|
||
final found = _findArrayDeep(Map<String, dynamic>.from(v), keys);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static int? _extractInt(Map<String, dynamic> json, List<String> keys) {
|
||
for (final key in keys) {
|
||
final v = json[key];
|
||
if (v != null) return v is int ? v : int.tryParse(v.toString());
|
||
}
|
||
for (final entry in json.entries) {
|
||
if (entry.value is Map<String, dynamic>) {
|
||
final found = _extractInt(entry.value as Map<String, dynamic>, keys);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? _findStringDeep(Map<String, dynamic> json, List<String> keys) {
|
||
for (final key in keys) {
|
||
final v = json[key];
|
||
if (v != null && v.toString().isNotEmpty) return v.toString();
|
||
}
|
||
for (final entry in json.entries) {
|
||
if (entry.value is Map<String, dynamic>) {
|
||
final found = _findStringDeep(
|
||
entry.value as Map<String, dynamic>,
|
||
keys,
|
||
);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
final fileProvider = StateNotifierProvider<FileNotifier, FileState>(
|
||
(ref) => FileNotifier(),
|
||
);
|