class FileUtils { static String safeDecodePathSegment(String value, {int maxPasses = 5}) { var decoded = value; for (var i = 0; i < maxPasses; i++) { try { final next = Uri.decodeComponent(decoded); if (next == decoded) break; decoded = next; } on FormatException { break; } on ArgumentError { break; } } return decoded; } static String decodePathForDisplay(String path) { return path .split('/') .map((segment) => safeDecodePathSegment(segment)) .join('/'); } static String toCloudreveUri(String path) { if (path.startsWith('cloudreve://')) return path; if (path == '/' || path.isEmpty) return 'cloudreve://my'; final cleanPath = path.startsWith('/') ? path.substring(1) : path; return 'cloudreve://my/$cleanPath'; } static String toCloudreveUriWithQuery( String path, Map queryParameters, ) { final uri = Uri.parse(toCloudreveUri(path)); return uri.replace(queryParameters: queryParameters).toString(); } static String getFileExtension(String fileName) { final dotIndex = fileName.lastIndexOf('.'); if (dotIndex == -1) return ''; return fileName.substring(dotIndex + 1).toLowerCase(); } static bool isImageFile(String fileName) { const imageExtensions = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'heic', ]; return imageExtensions.contains(getFileExtension(fileName)); } static bool isVideoFile(String fileName) { const videoExtensions = ['mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'wmv']; return videoExtensions.contains(getFileExtension(fileName)); } static bool isAudioFile(String fileName) { const audioExtensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']; return audioExtensions.contains(getFileExtension(fileName)); } static bool isPdfFile(String fileName) { return getFileExtension(fileName) == 'pdf'; } static bool isTextFile(String fileName) { const textExtensions = [ 'txt', 'md', 'json', 'xml', 'yaml', 'yml', 'ini', 'conf', ]; return textExtensions.contains(getFileExtension(fileName)); } static bool isCodeFile(String fileName) { const codeExtensions = [ 'js', 'ts', 'tsx', 'jsx', 'dart', 'java', 'py', 'c', 'cpp', 'h', 'hpp', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'html', 'css', 'scss', 'less', 'sql', 'sh', 'bat', ]; return codeExtensions.contains(getFileExtension(fileName)); } static bool isArchiveFile(String fileName) { const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']; return archiveExtensions.contains(getFileExtension(fileName)); } static bool isDocumentFile(String fileName) { const docExtensions = [ 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', ]; return docExtensions.contains(getFileExtension(fileName)); } static bool isPreviewable(String fileName) { return isImageFile(fileName) || isVideoFile(fileName) || isPdfFile(fileName) || isTextFile(fileName) || isCodeFile(fileName); } static String getMimeType(String fileName) { final ext = getFileExtension(fileName); final mimeTypes = { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml', 'mp4': 'video/mp4', 'webm': 'video/webm', 'mkv': 'video/x-matroska', 'avi': 'video/x-msvideo', 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'ogg': 'audio/ogg', 'pdf': 'application/pdf', 'txt': 'text/plain', 'json': 'application/json', 'xml': 'application/xml', 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', }; return mimeTypes[ext] ?? 'application/octet-stream'; } static String getFileIcon(String fileName) { if (isImageFile(fileName)) return 'assets/icons/image.svg'; if (isVideoFile(fileName)) return 'assets/icons/video.svg'; if (isAudioFile(fileName)) return 'assets/icons/audio.svg'; if (isPdfFile(fileName)) return 'assets/icons/pdf.svg'; if (isTextFile(fileName)) return 'assets/icons/text.svg'; if (isCodeFile(fileName)) return 'assets/icons/code.svg'; if (isArchiveFile(fileName)) return 'assets/icons/archive.svg'; if (isDocumentFile(fileName)) return 'assets/icons/document.svg'; return 'assets/icons/file.svg'; } }