fix(viewer): replace text arrow share icon with SVG share glyph

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 13:55:37 +08:00
parent 900ff2a74d
commit b353a3de06
54 changed files with 2130 additions and 14 deletions
+76
View File
@@ -10,6 +10,8 @@
/// 抛 [AppException](码表见 [ErrorCodes];l10n 文案解析留在 UI 层)。
library;
import 'dart:io' show File, Platform;
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -282,6 +284,80 @@ class ApiClient {
Future<CursorPage<AlbumSummary>> listAlbums({String? cursor, int? limit}) =>
_getPage('/v1/albums', AlbumSummary.fromJson, cursor: cursor, limit: limit);
/// 资产上传凭证(契约 AssetInitResponse)。
/// kind:cover | banner | logo;凭据结构与照片 presign 相同。
static const assetKindCover = 'cover';
static const assetKindBanner = 'banner';
Future<({String assetId, UploadCredential credential})>
initAlbumAsset(String albumId, {
required String kind,
required String filename,
required int bytes,
required String contentType,
}) async {
final resp = await _send(
() => _dio.post<Map<String, dynamic>>('/v1/albums/$albumId/assets', data: {
'kind': kind,
'filename': filename,
'bytes': bytes,
'contentType': contentType,
}),
);
final data = resp.data!;
return (
assetId: data['assetId'] as String,
credential: UploadCredential.fromJson(data['credential'] as Map<String, dynamic>),
);
}
/// 完成资产上传并激活(返回短期签名 URL)。
Future<String> completeAlbumAsset(String albumId, String assetId) async {
final resp = await _send(
() => _dio.post<Map<String, dynamic>>(
'/v1/albums/$albumId/assets/$assetId/complete',
),
);
return resp.data!['url'] as String;
}
/// 一步上传相册封面:选图 → presign → PUT → complete。
/// [filePath] 是本机源文件路径(由 singleImagePicker 返回)。
/// 返回封面短期 URL(仅用于即时预览;列表走 AlbumSummary.cover)。
Future<String> uploadAlbumCoverFromFile(String albumId, String filePath) async {
final file = File(filePath);
final bytes = await file.length();
final filename = filePath.split(Platform.pathSeparator).last;
final contentType = _guessContentType(filename);
final init = await initAlbumAsset(albumId,
kind: assetKindCover,
filename: filename,
bytes: bytes,
contentType: contentType);
final putRes = await Dio().put(
init.credential.url,
data: file.openRead(),
options: Options(headers: {
...init.credential.headers,
'Content-Length': bytes,
}),
);
if (putRes.statusCode != 200 && putRes.statusCode != 204) {
throw Exception('cover PUT failed: ${putRes.statusCode}');
}
return completeAlbumAsset(albumId, init.assetId);
}
String _guessContentType(String filename) {
final ext = filename.split('.').last.toLowerCase();
return switch (ext) {
'jpg' || 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'heic' => 'image/heic',
_ => 'image/jpeg',
};
}
/// operationId `createAlbum`(POST /albums → 201)。
Future<Album> createAlbum(AlbumCreateRequest req) async {
final resp = await _send(