import 'dart:collection'; import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../entities/photo.dart'; import '../page_result.dart'; class PhotoRepository { final String basePath; final Map> _map = HashMap(); final Map _updatedMap = HashMap(); PhotoRepository(this.basePath); /// 获取相册中所有照片(不分页,保留兼容) Future> getPhotosByAlbumId(int id) async { final all = await _loadPhotosForAlbum(id); return all; } /// 获取相册中的照片(分页) Future> getPhotosByAlbumIdPaged( int id, { int page = 1, int size = 20, }) async { final all = await _loadPhotosForAlbum(id); final total = all.length; final startIndex = (page - 1) * size; final endIndex = startIndex + size; final items = startIndex < total ? all.sublist(startIndex, endIndex > total ? total : endIndex) : []; return PageResult(items: items, total: total, page: page, size: size); } /// 内部方法:加载相册照片(带缓存) Future> _loadPhotosForAlbum(int id) async { if (_updatedMap.containsKey(id) && DateTime.now().difference(_updatedMap[id]!).inSeconds <= 60) { return _map[id]!; } _updatedMap[id] = DateTime.now(); final dir = Directory(basePath); if (!await dir.exists()) { return []; } Directory? albumDir; try { albumDir = await dir .list() .where((f) => f is Directory) .where((d) => d.path.hashCode == id) .first as Directory; } on StateError { return []; } // 获取相册中的所有文件 final files = await albumDir .list() .where((f) => f is File) .map((f) => f as File) .where((f) => _isImageOrVideo(f.path)) .toList(); // 并行处理所有文件的尺寸获取 final photoFutures = files.map((file) async { final stat = file.statSync(); final fileName = p.basename(file.path); final mimeType = _getMimeType(file.path); final dimensions = _getImageDimensions(file, mimeType); return Photo( id: file.path.hashCode, albumId: id, filePath: file.path, fileName: fileName, fileSize: stat.size, mimeType: mimeType, width: dimensions?.$1, height: dimensions?.$2, createdAt: stat.changed, ); }); final photos = await Future.wait(photoFutures); _map[id] = List.from(photos); return photos; } Future getPhotoById(int id) async { try { return _map.values.expand((l) => l).firstWhere((p) => p.id == id); } catch (e) {} final dir = Directory(basePath); if (!await dir.exists()) { return null; } File file; try { file = await dir .list(recursive: true) .where((f) => f is File) .map((f) => f as File) .firstWhere((f) => f.path.hashCode == id); } on StateError { // firstWhere throws StateError when no element is found return null; } final stat = file.statSync(); final mimeType = _getMimeType(file.path); final dimensions = _getImageDimensions(file, mimeType); return Photo( id: file.path.hashCode, albumId: file.parent.path.hashCode, filePath: file.path, fileName: p.basename(file.path), fileSize: stat.size, mimeType: mimeType, width: dimensions?.$1, height: dimensions?.$2, createdAt: stat.changed, ); } /// 获取图片的宽度和高度 /// 返回 (width, height) 或 null(如果是视频或无法解析) (int, int)? _getImageDimensions(File file, String mimeType) { // 视频文件不返回尺寸 if (mimeType.startsWith('video/')) { return null; } try { // 只读取文件头部的一小部分来获取尺寸 final raf = file.openSync(); try { // 读取前 32KB 应该足够获取图片尺寸 final bytesToRead = 32 * 1024; final buffer = Uint8List(bytesToRead); final bytesRead = raf.readIntoSync(buffer); final data = buffer.sublist(0, bytesRead); return _parseImageDimensions(data, mimeType); } finally { raf.closeSync(); } } catch (_) { // 如果无法解析图片,返回 null return null; } } /// 解析图片尺寸 /// 支持 JPEG, PNG, GIF, BMP, WebP 格式 (int, int)? _parseImageDimensions(Uint8List data, String mimeType) { try { switch (mimeType) { case 'image/jpeg': return _parseJpegDimensions(data); case 'image/png': return _parsePngDimensions(data); case 'image/gif': return _parseGifDimensions(data); case 'image/bmp': return _parseBmpDimensions(data); case 'image/webp': return _parseWebpDimensions(data); default: return null; } } catch (_) { return null; } } /// 解析 JPEG 图片尺寸 (int, int)? _parseJpegDimensions(Uint8List data) { if (data.length < 2 || data[0] != 0xFF || data[1] != 0xD8) { return null; // 不是有效的 JPEG } var offset = 2; while (offset < data.length - 1) { if (data[offset] != 0xFF) { offset++; continue; } final marker = data[offset + 1]; // SOF 标记 (Start Of Frame) if (marker >= 0xC0 && marker <= 0xC3 || marker >= 0xC5 && marker <= 0xC7 || marker >= 0xC9 && marker <= 0xCB || marker >= 0xCD && marker <= 0xCF) { if (offset + 9 < data.length) { final height = (data[offset + 5] << 8) | data[offset + 6]; final width = (data[offset + 7] << 8) | data[offset + 8]; return (width, height); } } // 跳过这个标记 if (marker >= 0xD0 && marker <= 0xD9) { offset += 2; } else if (marker == 0xFF) { offset += 2; } else { if (offset + 3 < data.length) { final length = (data[offset + 2] << 8) | data[offset + 3]; offset += 2 + length; } else { break; } } } return null; } /// 解析 PNG 图片尺寸 (int, int)? _parsePngDimensions(Uint8List data) { if (data.length < 24 || data[0] != 0x89 || data[1] != 0x50 || data[2] != 0x4E || data[3] != 0x47 || data[4] != 0x0D || data[5] != 0x0A || data[6] != 0x1A || data[7] != 0x0A) { return null; // 不是有效的 PNG } // IHDR chunk 在第 16-23 字节 final width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; final height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; return (width, height); } /// 解析 GIF 图片尺寸 (int, int)? _parseGifDimensions(Uint8List data) { if (data.length < 10 || data[0] != 0x47 || data[1] != 0x49 || data[2] != 0x46 || data[3] != 0x38) { return null; // 不是有效的 GIF } final width = data[6] | (data[7] << 8); final height = data[8] | (data[9] << 8); return (width, height); } /// 解析 BMP 图片尺寸 (int, int)? _parseBmpDimensions(Uint8List data) { if (data.length < 26 || data[0] != 0x42 || data[1] != 0x4D) { return null; // 不是有效的 BMP } final width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); final height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); return (width, height.abs()); // BMP 高度可能是负数 } /// 解析 WebP 图片尺寸 (int, int)? _parseWebpDimensions(Uint8List data) { if (data.length < 30 || data[0] != 0x52 || data[1] != 0x49 || data[2] != 0x46 || data[3] != 0x46 || data[8] != 0x57 || data[9] != 0x45 || data[10] != 0x42 || data[11] != 0x50) { return null; // 不是有效的 WebP } final type = String.fromCharCodes(data.sublist(12, 16)); if (type == 'VP8 ') { // Lossy WebP if (data.length < 30) return null; final key = (data[23] << 8) | data[22]; if (key != 0x9D01) return null; final width = data[26] | (data[27] << 8); final height = data[28] | (data[29] << 8); return (width, height); } else if (type == 'VP8L') { // Lossless WebP if (data.length < 29) return null; if (data[21] != 0x2F) return null; final width = 1 + ((data[22] | (data[23] << 8)) & 0x3FFF); final height = 1 + (((data[23] >> 6) | (data[24] << 2) | (data[25] << 10)) & 0x3FFF); return (width, height); } else if (type == 'VP8X') { // Extended WebP if (data.length < 30) return null; final width = 1 + (data[24] | (data[25] << 8) | (data[26] << 16)); final height = 1 + (data[27] | (data[28] << 8) | (data[29] << 16)); return (width, height); } return null; } Future deletePhoto(int id) { // TODO: implement deletePhoto throw UnimplementedError(); } bool _isImageOrVideo(String path) { final ext = p.extension(path).toLowerCase(); return [ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.mp4', '.avi', '.mov', '.mkv', ].contains(ext); } String _getMimeType(String path) { final ext = p.extension(path).toLowerCase(); switch (ext) { case '.jpg': case '.jpeg': return 'image/jpeg'; case '.png': return 'image/png'; case '.gif': return 'image/gif'; case '.bmp': return 'image/bmp'; case '.webp': return 'image/webp'; case '.mp4': return 'video/mp4'; case '.avi': return 'video/avi'; case '.mov': return 'video/quicktime'; case '.mkv': return 'video/x-matroska'; default: return 'application/octet-stream'; } } }