fix(viewer): replace text arrow share icon with SVG share glyph
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user