fix(app): camera preview distortion — swap sensor W/H for portrait
previewSize 报横向(W>H),竖屏直用会把画面拉扁。交换宽高转纵向比例再 FittedBox cover 铺满(裁切不变形)。397/397 全绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
# /opt/photo-live/deploy/docker-compose-cn.yml —— 正式环境 cn 栈(2026-09-24)。
|
||||||
|
# 基于 infra/deploy/docker-compose.cn.yml 改造(同 ov 版偏差登记):
|
||||||
|
# ① 自管 mysql:8.4 + redis:7(1Panel MySQL 是 5.7.44 不满足基线)
|
||||||
|
# ② 独立桥网络 photo-net(与机上其它项目隔离)
|
||||||
|
# ③ web 回环端口 18088(避开 1Panel phpMyAdmin 的 8089 段)
|
||||||
|
# ④ mysql 内存调优:cn 机仅 3.5G(可用 ~1.4G),关 performance_schema、
|
||||||
|
# buffer pool 显式 128M(另建 2G swap 兜底,见部署记录)
|
||||||
|
# cn 侧多两个角色:worker(sharp 图片流水线)+ sync(跨境推送/对账/统计回传)。
|
||||||
|
# sync 需出网可达:OV_API_URL(https 443)与 DATABASE_URL_OV(47.254.176.10:13306,
|
||||||
|
# 安全组白名单 cn 出口 IP)。
|
||||||
|
|
||||||
|
name: photo-live-cn
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options: { max-size: "10m", max-file: "3" }
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4
|
||||||
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_bin --performance-schema=OFF --innodb-buffer-pool-size=128M
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
|
MYSQL_DATABASE: photo_cn
|
||||||
|
MYSQL_USER: photo_cn
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_APP_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mysql-cn-data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 24
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 5s, retries: 12 }
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
api-cn:
|
||||||
|
image: ${SERVER_IMAGE}
|
||||||
|
command: ["api-cn"]
|
||||||
|
init: true
|
||||||
|
env_file: .env.production-cn
|
||||||
|
environment:
|
||||||
|
PORT: "8080" # 容器内固定;对外经 web 反代
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql: { condition: service_healthy }
|
||||||
|
redis: { condition: service_healthy }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fs", "http://127.0.0.1:8080/v1/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: ${SERVER_IMAGE}
|
||||||
|
command: ["worker"]
|
||||||
|
init: true
|
||||||
|
env_file: .env.production-cn
|
||||||
|
# 无 HTTP 端口:健康面由 restart 策略 + 日志监控承担(RUNBOOK.md)
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql: { condition: service_healthy }
|
||||||
|
redis: { condition: service_healthy }
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
sync:
|
||||||
|
image: ${SERVER_IMAGE}
|
||||||
|
command: ["sync"]
|
||||||
|
init: true
|
||||||
|
env_file: .env.production-cn
|
||||||
|
# 需出网:OV_API_URL(443) + DATABASE_URL_OV(13306,待安全组放行)
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql: { condition: service_healthy }
|
||||||
|
redis: { condition: service_healthy }
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${WEB_IMAGE}
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
API_UPSTREAM: http://api-cn:8080 # /v1 反代目标(含 WS /v1/realtime)
|
||||||
|
ROOT_REDIRECT: /admin/ # cn 根路径直达后台
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18088:80" # 1Panel OpenResty 反代到此(站点已建)
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
api-cn: { condition: service_healthy }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-cn-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
photo-net: { driver: bridge }
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# /opt/photo-live/deploy/docker-compose-ov.yml —— 正式环境 ov 栈(2026-09-24)。
|
||||||
|
# 基于 infra/deploy/docker-compose-ov.yml 改三点(计划 §2 T0 偏差登记):
|
||||||
|
# ① 自管 mysql:8.4 + redis:7(1Panel MySQL 是 5.7.44,不满足 8.4/utf8mb4_0900_bin 基线)
|
||||||
|
# ② 独立桥网络 photo-net(不挂 1panel-network,与机上其它项目隔离)
|
||||||
|
# ③ web-ov 回环端口 18089(8089 被 1Panel phpMyAdmin 占用)
|
||||||
|
# ④ mysql 公网映射 13306:cn 侧 sync 跨境直连读库——安全组仅放行 101.37.116.184!
|
||||||
|
# 同目录配套:.env(SERVER_IMAGE/WEB_IMAGE/MYSQL_ROOT_PASSWORD/MYSQL_APP_PASSWORD)
|
||||||
|
# 与 .env.production-ov(应用 env,模板=仓库根 .env.production.example-ov)。
|
||||||
|
|
||||||
|
name: photo-live-ov
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options: { max-size: "10m", max-file: "3" }
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4
|
||||||
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_bin
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
|
MYSQL_DATABASE: photo_ov
|
||||||
|
MYSQL_USER: photo_ov
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_APP_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:13306:3306" # 仅安全组白名单 cn IP 可达(阿里云控制台)
|
||||||
|
volumes:
|
||||||
|
- mysql-ov-data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 24
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 5s, retries: 12 }
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
api-ov:
|
||||||
|
image: ${SERVER_IMAGE}
|
||||||
|
command: ["api-ov"]
|
||||||
|
init: true
|
||||||
|
env_file: .env.production-ov
|
||||||
|
environment:
|
||||||
|
PORT_OV: "8081" # main-ov.ts 读 PORT_OV;容器内固定
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
mysql: { condition: service_healthy }
|
||||||
|
redis: { condition: service_healthy }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fs", "http://127.0.0.1:8081/v1/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
web-ov:
|
||||||
|
image: ${WEB_IMAGE}
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
API_UPSTREAM: http://api-ov:8081 # /v1 反代目标(含 WS /v1/realtime)
|
||||||
|
ROOT_REDIRECT: /index.html # 观众端安全自指
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18089:80" # 1Panel OpenResty 反代到此(站点已建)
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
api-ov: { condition: service_healthy }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
logging: *default-logging
|
||||||
|
networks: [photo-net]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-ov-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
photo-net: { driver: bridge }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SERVER_IMAGE=photo-live-server:30a3520-20260924
|
||||||
|
WEB_IMAGE=photo-live-web:30a3520-20260924-cn
|
||||||
|
MYSQL_ROOT_PASSWORD=2d88f24a008eac264944141c69b61b22dfbd00aaecd133c7
|
||||||
|
MYSQL_APP_PASSWORD=20a6f359f685b6a0ae725698d722cfc00357503d7a27ecd7
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SERVER_IMAGE=photo-live-server:30a3520-20260924
|
||||||
|
WEB_IMAGE=photo-live-web:30a3520-20260924-ov
|
||||||
|
MYSQL_ROOT_PASSWORD=127bd53921e208fbc0f9ff9c0b64a5fa71069ef0b083c617
|
||||||
|
MYSQL_APP_PASSWORD=70e4a6edc39800bd160723cb62a9a8e6a91d4c8f4c0ce399
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一次性:PC 适配验证测试相册(真实链路上传 6 张不同宽高比测试图)
|
||||||
|
set -eu
|
||||||
|
CN=https://live-album.inside.hnxinyouhe.com
|
||||||
|
OV=https://live-album.outside.hnxinyouhe.com
|
||||||
|
IMGDIR="C:/Users/30997/AppData/Local/Temp/pc-test"
|
||||||
|
PW=$(cat /d/project/photo/.env.prod-secrets/ADMIN_PASSWORD | tr -d '\n\r')
|
||||||
|
|
||||||
|
extract() { printf '%s' "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" | head -1; }
|
||||||
|
|
||||||
|
BODY=$(curl -sS $CN/v1/auth/password -H 'content-type: application/json' -d "{\"account\":\"admin\",\"password\":\"$PW\"}")
|
||||||
|
TOKEN=$(extract "$BODY" sessionToken); [ -n "$TOKEN" ] || { echo "login FAIL: $BODY"; exit 1; }
|
||||||
|
echo "login ok"
|
||||||
|
|
||||||
|
BODY=$(curl -sS $CN/v1/albums -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
|
||||||
|
-d '{"translations":[{"locale":"zh-CN","title":"PC适配验证(测试相册可隐藏)","description":"桌面端样式优化验证用,6 张不同宽高比测试图"}],"defaultLocale":"zh-CN","startAt":"2026-09-24T00:00:00.000Z"}')
|
||||||
|
ALBUM_ID=$(extract "$BODY" id); PUB=$(extract "$BODY" publicToken)
|
||||||
|
[ -n "$ALBUM_ID" ] || { echo "album FAIL: $BODY"; exit 1; }
|
||||||
|
echo "album: $ALBUM_ID publicToken: $PUB"
|
||||||
|
|
||||||
|
i=0
|
||||||
|
for f in "$IMGDIR"/img*.jpg; do
|
||||||
|
i=$((i+1))
|
||||||
|
BYTES=$(stat -c %s "$f")
|
||||||
|
BODY=$(curl -sS $CN/v1/uploads -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
|
||||||
|
-d "{\"albumId\":\"$ALBUM_ID\",\"clientTaskId\":\"pc-test-$i\",\"filename\":\"pc-$i.jpg\",\"bytes\":$BYTES,\"contentType\":\"image/jpeg\"}")
|
||||||
|
UP=$(extract "$BODY" uploadId); URL=$(extract "$BODY" url)
|
||||||
|
[ -n "$UP" ] || { echo "init$i FAIL: $BODY"; exit 1; }
|
||||||
|
CODE=$(curl -sS -X PUT "$URL" -H 'Content-Type: image/jpeg' --data-binary @"$f" -o /dev/null -w '%{http_code}')
|
||||||
|
[ "${CODE:0:1}" = 2 ] || { echo "PUT$i FAIL: $CODE"; exit 1; }
|
||||||
|
CODE=$(curl -sS -X POST $CN/v1/uploads/$UP/complete -H "authorization: Bearer $TOKEN" -o /dev/null -w '%{http_code}')
|
||||||
|
[ "${CODE:0:1}" = 2 ] || { echo "complete$i FAIL: $CODE"; exit 1; }
|
||||||
|
echo "img$i uploaded ($BYTES B)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "waiting for ov visibility..."
|
||||||
|
for t in $(seq 1 30); do
|
||||||
|
N=$(curl -sS "$OV/v1/public/albums/$PUB/photos" | grep -c photoId || true)
|
||||||
|
[ "$N" -ge 6 ] && { echo "ov visible: $N photos (${t}x2s)"; break; }
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "VIEWER_URL=https://live-album.outside.hnxinyouhe.com/a/$PUB"
|
||||||
@@ -62,14 +62,16 @@ class CameraPluginSource extends CaptureSource {
|
|||||||
throw CameraException('noCamera', 'no camera available');
|
throw CameraException('noCamera', 'no camera available');
|
||||||
}
|
}
|
||||||
CameraDescription pick() => cameras.firstWhere(
|
CameraDescription pick() => cameras.firstWhere(
|
||||||
(c) => c.lensDirection ==
|
(c) =>
|
||||||
(preferFront
|
c.lensDirection ==
|
||||||
? CameraLensDirection.front
|
(preferFront ? CameraLensDirection.front : CameraLensDirection.back),
|
||||||
: CameraLensDirection.back),
|
orElse: () => cameras.first,
|
||||||
orElse: () => cameras.first,
|
);
|
||||||
);
|
final controller = CameraController(
|
||||||
final controller = CameraController(pick(), ResolutionPreset.max,
|
pick(),
|
||||||
enableAudio: false);
|
ResolutionPreset.max,
|
||||||
|
enableAudio: false,
|
||||||
|
);
|
||||||
await controller.initialize();
|
await controller.initialize();
|
||||||
return CameraPluginSource._(controller);
|
return CameraPluginSource._(controller);
|
||||||
}
|
}
|
||||||
@@ -84,15 +86,17 @@ class CameraPluginSource extends CaptureSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildPreview() {
|
Widget buildPreview() {
|
||||||
// 纵横比校正(§4 页面 13):预览按传感器尺寸布局,再 cover 铺满——
|
// 纵横比校正(§4 页面 13):传感器 previewSize 报的是**横向**(width >
|
||||||
// CameraPreview 直填父约束会把画面拉扁/拉高。
|
// height),竖屏 App 直接用它会把画面拉扁——需交换宽高转为纵向比例,
|
||||||
|
// 再 FittedBox cover 铺满(裁切不变形)。
|
||||||
final size = _controller.value.previewSize;
|
final size = _controller.value.previewSize;
|
||||||
Widget preview = CameraPreview(_controller);
|
Widget preview = CameraPreview(_controller);
|
||||||
if (size != null && size.width > 0 && size.height > 0) {
|
if (size != null && size.width > 0 && size.height > 0) {
|
||||||
preview = FittedBox(
|
preview = FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: SizedBox(width: size.width, height: size.height, child: preview),
|
// 交换:竖屏用 height 作宽、width 作高(传感器横向 → 纵向)。
|
||||||
|
child: SizedBox(width: size.height, height: size.width, child: preview),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return preview;
|
return preview;
|
||||||
@@ -102,9 +106,8 @@ class CameraPluginSource extends CaptureSource {
|
|||||||
bool get supportsFlash => true;
|
bool get supportsFlash => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setFlashEnabled(bool enabled) => _controller.setFlashMode(
|
Future<void> setFlashEnabled(bool enabled) =>
|
||||||
enabled ? FlashMode.always : FlashMode.off,
|
_controller.setFlashMode(enabled ? FlashMode.always : FlashMode.off);
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() => _controller.dispose();
|
Future<void> dispose() => _controller.dispose();
|
||||||
@@ -113,7 +116,8 @@ class CameraPluginSource extends CaptureSource {
|
|||||||
/// 拍摄源工厂 Provider(测试 override 为替身)。
|
/// 拍摄源工厂 Provider(测试 override 为替身)。
|
||||||
final captureSourceFactoryProvider =
|
final captureSourceFactoryProvider =
|
||||||
Provider<Future<CaptureSource> Function({bool preferFront})>(
|
Provider<Future<CaptureSource> Function({bool preferFront})>(
|
||||||
(ref) => CameraPluginSource.create);
|
(ref) => CameraPluginSource.create,
|
||||||
|
);
|
||||||
|
|
||||||
// ── 连拍页 ────────────────────────────────────────────────────────────────
|
// ── 连拍页 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -207,9 +211,11 @@ class _CapturePageState extends ConsumerState<CapturePage> {
|
|||||||
});
|
});
|
||||||
} on Object catch (error) {
|
} on Object catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _phase = _isPermissionDenied(error)
|
setState(
|
||||||
? _CameraPhase.denied
|
() => _phase = _isPermissionDenied(error)
|
||||||
: _CameraPhase.failed);
|
? _CameraPhase.denied
|
||||||
|
: _CameraPhase.failed,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,23 +253,25 @@ class _CapturePageState extends ConsumerState<CapturePage> {
|
|||||||
final source = _source;
|
final source = _source;
|
||||||
if (source == null || _phase != _CameraPhase.ready) return;
|
if (source == null || _phase != _CameraPhase.ready) return;
|
||||||
final categoryId = _categoryId;
|
final categoryId = _categoryId;
|
||||||
_shotTail = _shotTail.then((_) async {
|
_shotTail = _shotTail
|
||||||
final photo = await source.capture();
|
.then((_) async {
|
||||||
if (!mounted) return;
|
final photo = await source.capture();
|
||||||
// 最近一张缩略图(拍摄原文件;缺失走占位)。
|
if (!mounted) return;
|
||||||
setState(() => _lastShotPath = photo.path);
|
// 最近一张缩略图(拍摄原文件;缺失走占位)。
|
||||||
final enqueued = await enqueueForUpload(
|
setState(() => _lastShotPath = photo.path);
|
||||||
ref,
|
final enqueued = await enqueueForUpload(
|
||||||
accountId: widget.target.accountId,
|
ref,
|
||||||
albumId: widget.target.albumId,
|
accountId: widget.target.accountId,
|
||||||
categoryId: categoryId,
|
albumId: widget.target.albumId,
|
||||||
sourcePaths: [photo.path],
|
categoryId: categoryId,
|
||||||
);
|
sourcePaths: [photo.path],
|
||||||
// 入队成功:短暂反馈(轻触 + 200ms 浮层),不逐张弹确认(§4.5)。
|
);
|
||||||
if (enqueued > 0 && mounted) _flashEnqueued();
|
// 入队成功:短暂反馈(轻触 + 200ms 浮层),不逐张弹确认(§4.5)。
|
||||||
}).catchError((Object _) {
|
if (enqueued > 0 && mounted) _flashEnqueued();
|
||||||
// 单张失败吞掉(连拍不中断;失败任务/错误在徽章与队列页可见)。
|
})
|
||||||
});
|
.catchError((Object _) {
|
||||||
|
// 单张失败吞掉(连拍不中断;失败任务/错误在徽章与队列页可见)。
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 入队成功反馈:触觉轻点 + 顶部短暂浮层(200ms 后自动消退)。
|
/// 入队成功反馈:触觉轻点 + 顶部短暂浮层(200ms 后自动消退)。
|
||||||
@@ -488,14 +496,19 @@ class _TopBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: MaterialLocalizations.of(context)
|
tooltip: MaterialLocalizations.of(
|
||||||
.backButtonTooltip, // 语义标签;深色界面固定白色前景。
|
context,
|
||||||
|
).backButtonTooltip, // 语义标签;深色界面固定白色前景。
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: onBack,
|
onPressed: onBack,
|
||||||
),
|
),
|
||||||
const SizedBox(width: kSpace4),
|
const SizedBox(width: kSpace4),
|
||||||
const Icon(Icons.photo_album_outlined, size: 16, color: Colors.white70),
|
const Icon(
|
||||||
|
Icons.photo_album_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -609,10 +622,7 @@ class _Badge extends StatelessWidget {
|
|||||||
color: color,
|
color: color,
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(label, style: TextStyle(color: foreground, fontSize: 12)),
|
||||||
label,
|
|
||||||
style: TextStyle(color: foreground, fontSize: 12),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,11 +721,8 @@ class _LastShotButton extends StatelessWidget {
|
|||||||
content = Image.file(
|
content = Image.file(
|
||||||
File(lastShotPath!),
|
File(lastShotPath!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, _, _) => const Icon(
|
errorBuilder: (_, _, _) =>
|
||||||
Icons.image_outlined,
|
const Icon(Icons.image_outlined, size: 24, color: Colors.white),
|
||||||
size: 24,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 106 KiB |
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user