初始化项目
This commit is contained in:
317
bin/domain/repositories/photo_repository.dart
Normal file
317
bin/domain/repositories/photo_repository.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../entities/photo.dart';
|
||||
|
||||
class PhotoRepository {
|
||||
final String basePath;
|
||||
|
||||
PhotoRepository(this.basePath);
|
||||
|
||||
Future<List<Photo>> getPhotosByAlbumId(int id) async {
|
||||
final dir = Directory(basePath);
|
||||
|
||||
if (!await dir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Directory? albumDir;
|
||||
try {
|
||||
albumDir = await dir
|
||||
.list()
|
||||
.where((f) => f is Directory)
|
||||
.where((d) => p.basename(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: fileName.hashCode,
|
||||
albumId: id,
|
||||
filePath: file.path,
|
||||
fileName: fileName,
|
||||
fileSize: stat.size,
|
||||
mimeType: mimeType,
|
||||
width: dimensions?.$1,
|
||||
height: dimensions?.$2,
|
||||
createdAt: stat.changed,
|
||||
);
|
||||
});
|
||||
|
||||
return Future.wait(photoFutures);
|
||||
}
|
||||
|
||||
Future<Photo?> getPhotoById(int id) async {
|
||||
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) => p.basename(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: p.basename(file.path).hashCode,
|
||||
albumId: p.basename(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<void> 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user