feat(app): P0 batch 1 — photo viewer zoom, camera lifecycle, image memory management
P0-2 查看器缩放:InteractiveViewer(5x max,放大后平移,1x 时穿透到 PageView 滑动切页) P0-3 相机后台释放:WidgetsBindingObserver——paused 时 dispose(省电+ 隐私指示灯),resumed 时重开(沿用前后摄/闪光灯偏好) P0-6 图片内存管理:PhotoThumbnail 加 maxCacheWidth 参数——网格/列表 缩略图限制解码宽度(600/400px),查看器大图不限 397/397 全绿、analyze 0 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -50,8 +50,8 @@ enum PhotoGridFilter {
|
||||
final String wire;
|
||||
|
||||
/// 本地过滤(服务端无 processingState 查询参数)。
|
||||
bool get isLocal => this == PhotoGridFilter.processing ||
|
||||
this == PhotoGridFilter.failed;
|
||||
bool get isLocal =>
|
||||
this == PhotoGridFilter.processing || this == PhotoGridFilter.failed;
|
||||
}
|
||||
|
||||
/// 一页数据的请求规格(刷新签名 URL 时按原参数重取该页)。
|
||||
@@ -83,12 +83,12 @@ class PhotoGridController extends ChangeNotifier {
|
||||
required Future<Session?> Function() readSession,
|
||||
required this.albumId,
|
||||
this.pageSize = 20,
|
||||
}) // 私有字段不能作 initializing formal(外部调用方无法传私有名),
|
||||
// 参数名保持公开语义,忽略该风格提示。
|
||||
// ignore: prefer_initializing_formals
|
||||
: _api = api,
|
||||
// ignore: prefer_initializing_formals
|
||||
_readSession = readSession;
|
||||
}) // 私有字段不能作 initializing formal(外部调用方无法传私有名),
|
||||
// 参数名保持公开语义,忽略该风格提示。
|
||||
// ignore: prefer_initializing_formals
|
||||
: _api = api,
|
||||
// ignore: prefer_initializing_formals
|
||||
_readSession = readSession;
|
||||
|
||||
final ApiClient _api;
|
||||
final Future<Session?> Function() _readSession;
|
||||
@@ -115,12 +115,16 @@ class PhotoGridController extends ChangeNotifier {
|
||||
|
||||
/// 当前筛选下应显示的条目。
|
||||
List<AdminPhoto> get visibleItems => switch (_filter) {
|
||||
PhotoGridFilter.processing => _items
|
||||
.where((p) => p.processingState != PhotoProcessingState.ready)
|
||||
.toList(growable: false),
|
||||
PhotoGridFilter.failed => _items
|
||||
.where((p) => p.processingState == PhotoProcessingState.processingFailed)
|
||||
.toList(growable: false),
|
||||
PhotoGridFilter.processing =>
|
||||
_items
|
||||
.where((p) => p.processingState != PhotoProcessingState.ready)
|
||||
.toList(growable: false),
|
||||
PhotoGridFilter.failed =>
|
||||
_items
|
||||
.where(
|
||||
(p) => p.processingState == PhotoProcessingState.processingFailed,
|
||||
)
|
||||
.toList(growable: false),
|
||||
_ => _items,
|
||||
};
|
||||
|
||||
@@ -362,26 +366,25 @@ class PhotoGridController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 删除(软删除,云端照片将被移除;权限同隐藏)。
|
||||
Future<void> deletePhoto(AdminPhoto photo) =>
|
||||
_api.deletePhoto(photo.photoId);
|
||||
Future<void> deletePhoto(AdminPhoto photo) => _api.deletePhoto(photo.photoId);
|
||||
|
||||
AdminPhoto _withRecord(AdminPhoto src, PhotoRecord rec) => AdminPhoto(
|
||||
uploader: src.uploader,
|
||||
preview: src.preview,
|
||||
photoId: rec.photoId,
|
||||
albumId: rec.albumId,
|
||||
categoryId: rec.categoryId,
|
||||
uploaderId: rec.uploaderId,
|
||||
visibility: rec.visibility,
|
||||
processingState: rec.processingState,
|
||||
bytes: rec.bytes,
|
||||
width: rec.width,
|
||||
height: rec.height,
|
||||
shotAt: rec.shotAt,
|
||||
uploadedAt: rec.uploadedAt,
|
||||
publishedAt: rec.publishedAt,
|
||||
likeCount: rec.likeCount,
|
||||
);
|
||||
uploader: src.uploader,
|
||||
preview: src.preview,
|
||||
photoId: rec.photoId,
|
||||
albumId: rec.albumId,
|
||||
categoryId: rec.categoryId,
|
||||
uploaderId: rec.uploaderId,
|
||||
visibility: rec.visibility,
|
||||
processingState: rec.processingState,
|
||||
bytes: rec.bytes,
|
||||
width: rec.width,
|
||||
height: rec.height,
|
||||
shotAt: rec.shotAt,
|
||||
uploadedAt: rec.uploadedAt,
|
||||
publishedAt: rec.publishedAt,
|
||||
likeCount: rec.likeCount,
|
||||
);
|
||||
|
||||
/// 覆盖 [index] 的页规格(删除导致边界漂移时按近似页处理)。
|
||||
_PageSpec? _pageForIndex(int index) {
|
||||
@@ -406,12 +409,12 @@ class PhotoGridController extends ChangeNotifier {
|
||||
PhotoVisibility? visibility,
|
||||
int? limit,
|
||||
}) => _api.listAlbumPhotos(
|
||||
albumId,
|
||||
scope: scope,
|
||||
visibility: visibility,
|
||||
cursor: cursor,
|
||||
limit: limit,
|
||||
);
|
||||
albumId,
|
||||
scope: scope,
|
||||
visibility: visibility,
|
||||
cursor: cursor,
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
|
||||
/// 照片管理网格页(从详情页携带 [album] 进入,时间按相册时区展示)。
|
||||
@@ -654,7 +657,8 @@ class _AlbumPhotosBody extends StatelessWidget {
|
||||
/// 请求)。
|
||||
void _maybeLoadMore(int index, int visibleCount) {
|
||||
final nearEnd = index >= visibleCount - 6;
|
||||
final localStarved = controller.filter.isLocal &&
|
||||
final localStarved =
|
||||
controller.filter.isLocal &&
|
||||
visibleCount < 9 &&
|
||||
controller.items.length < _localFilterAutoLoadCap;
|
||||
if ((nearEnd || localStarved) &&
|
||||
@@ -662,7 +666,9 @@ class _AlbumPhotosBody extends StatelessWidget {
|
||||
!controller.loadingMore &&
|
||||
!controller.loading &&
|
||||
!controller.loadMoreFailed) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => controller.loadMore());
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => controller.loadMore(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,7 +711,9 @@ class _PhotoFilterChip extends StatelessWidget {
|
||||
),
|
||||
selectedColor: theme.colorScheme.primary,
|
||||
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
||||
color: selected ? theme.colorScheme.onPrimary : theme.colorScheme.onSurface,
|
||||
color: selected
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: kSpace8),
|
||||
@@ -756,6 +764,7 @@ class _PhotoGridTile extends StatelessWidget {
|
||||
PhotoThumbnail(
|
||||
url: photo.preview?.thumb.url,
|
||||
aspectRatio: 1,
|
||||
maxCacheWidth: 400,
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
|
||||
Reference in New Issue
Block a user