Compare commits

...

3 Commits

Author SHA1 Message Date
wangjia e6b4ec0459 fix(ci/client): mac 端 job runs-on mac→nas(匹配实际 runner label)
Deploy Client / build-windows (push) Successful in 1m52s
Deploy Client / build-android (push) Successful in 2m4s
Deploy Client / build-macos (push) Successful in 3m38s
Deploy Client / build-ios (push) Failing after 1m57s
Deploy Client / release-deploy (push) Successful in 2m0s
mac runner(mac-pangolin-2)启动脚本注册 label 为 nas:host;此前靠手动加的 mac
label 让 runs-on:mac 命中,但 runner 重启(经启动脚本)会重注册成 nas、丢掉 mac
label → build-android/macos/ios/release-deploy 无 runner 可接、永久排队。改成
runs-on:nas 匹配实际 label,抗重启漂移(nas runner 即这台 mac,唯一)。windows
job 仍 runs-on:windows(windows runner)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 20:07:16 +08:00
wangjia 0b86138b55 feat(client/update): 更新提示改 jiu 式顶部横条(非强制不再弹窗)
Deploy Client / build-windows (push) Successful in 1m47s
Deploy Client / build-android (push) Has been cancelled
Deploy Client / build-macos (push) Has been cancelled
Deploy Client / build-ios (push) Has been cancelled
Deploy Client / release-deploy (push) Has been cancelled
按需求去掉居中弹窗,改成不打断的顶部 banner(对照 jiu 黄条):
- 非强制更新 → home_shell 顶部 UpdateBanner「发现新版本 vX + 立即更新 + ×」;
  「立即更新」App 内下载安装,「×」忽略此版本(banner 隐藏,更新版本会重现)
- 强制更新(force_update)→ 仍走不可关闭弹窗 showUpdateDialog
- 忽略状态改 dismissedUpdateVersionProvider(按版本号,响应式);去掉 notifier
  的 _dismissed
- 设置页「检查更新」有更新时:清忽略版本→banner 显示 + toast,不再弹窗
- 新增 6 语「立即更新」文案。flutter analyze 仅 2 个既有 withOpacity info

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 19:19:05 +08:00
wangjia aaffe85a4f fix(ci/windows): 同步 Flutter app 版本到 tag(原卡在 pubspec 1.0.48)
compile-windows 此前只 sed Inno 安装器 MyAppVersion,没改 Flutter pubspec 版本,
导致 Windows 包内 package_info 永远是 committed 的 1.0.48 → 设置页显示旧版 +
自动更新永远判"有新版"。补上 build number 计算 + sed 复制后的 pubspec.yaml
(GNU sed,windows runner),与 android/macos 对齐。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 19:05:31 +08:00
7 changed files with 129 additions and 32 deletions
+4 -4
View File
@@ -34,7 +34,7 @@ concurrency:
jobs:
build-android:
runs-on: mac
runs-on: nas
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
@@ -98,7 +98,7 @@ jobs:
# simply picks up whatever artifacts DO exist — an absent "macos"
# artifact is not an error there.
build-macos:
runs-on: mac
runs-on: nas
continue-on-error: true
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
@@ -129,7 +129,7 @@ jobs:
path: dist/
build-ios:
runs-on: mac
runs-on: nas
continue-on-error: true
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
@@ -160,7 +160,7 @@ jobs:
# (对应平台下载保留 pangolin1 上一版,待可用时下个 client-v* 追上)。
release-deploy:
needs: [build-android]
runs-on: mac
runs-on: nas
steps:
- name: Checkout
uses: actions/checkout@v4
+18
View File
@@ -308,6 +308,24 @@ extension AppLangMisc on AppLang {
return 'Cancelar';
}
}
/// 更新 banner:「立即更新」。
String get updateNow {
switch (this) {
case AppLang.zh:
return '立即更新';
case AppLang.en:
return 'Update now';
case AppLang.ja:
return '今すぐ更新';
case AppLang.ko:
return '지금 업데이트';
case AppLang.ru:
return 'Обновить';
case AppLang.es:
return 'Actualizar';
}
}
}
/// 全部界面文案的抽象契约。zh / en 各实现一份。
+5 -4
View File
@@ -15,10 +15,8 @@ import '../state/settings_provider.dart';
import '../state/update_provider.dart';
import '../widgets/pangolin_icons.dart';
import '../widgets/pangolin_toast.dart';
import '../widgets/update_dialog.dart';
/// 「检查更新」手动触发:拉取 `$kApiBaseUrl/version`,失败/无更新走轻提示,
/// 有更新则弹 [showUpdateDialog]
/// 有更新则清掉「已忽略版本」→ 顶部更新 banner 显示 + toast(不弹窗)
Future<void> _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) async {
final info = await ref.read(updateProvider.notifier).forceCheck();
if (!context.mounted) return;
@@ -30,7 +28,10 @@ Future<void> _checkForUpdate(BuildContext context, WidgetRef ref, AppText t) asy
showPangolinToast(context, t.updateUpToDate);
return;
}
await showUpdateDialog(context, t, info);
// 有更新:清掉「已忽略版本」→ 顶部 banner 重新显示(不弹窗);toast 提示一下。
// 强制更新由 home_shell 的 listen 走不可关闭弹窗,这里无需处理。
ref.read(dismissedUpdateVersionProvider.notifier).state = null;
showPangolinToast(context, t.updateAvailableTitle(info.latestVersion));
}
class SettingsPage extends ConsumerWidget {
+22 -13
View File
@@ -22,27 +22,36 @@ class HomeShell extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
// 启动自动检查更新:watch 惰性拉起 updateProvider(延迟 3s→查→1h 轮询);
// 有新版且本次会话未忽略时弹更新对话框。非强制更新弹前即标 dismiss,避免
// rebuild 重复弹(下轮轮询会重置);强制更新走不可关闭弹窗。
// 启动自动检查更新:watch 惰性拉起 updateProvider(延迟 3s→查→1h 轮询)
// 非强制更新 → 顶部 banner(见下,不打断);强制更新 → 不可关闭弹窗。
ref.listen<AsyncValue<AppUpdateInfo?>>(updateProvider, (prev, next) {
final info = next.valueOrNull;
if (info == null || !info.hasUpdate) return;
final notifier = ref.read(updateProvider.notifier);
if (notifier.isDismissed) return;
if (!info.forceUpdate) notifier.dismiss();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
showUpdateDialog(context, ref.read(appTextProvider), info);
});
if (info != null && info.hasUpdate && info.forceUpdate) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showUpdateDialog(context, ref.read(appTextProvider), info);
}
});
}
});
ref.watch(updateProvider); // 激活 provider(惰性 build)
final info = ref.watch(updateProvider).valueOrNull;
final dismissedVer = ref.watch(dismissedUpdateVersionProvider);
final showBanner = info != null &&
info.hasUpdate &&
!info.forceUpdate &&
dismissedVer != info.latestVersion;
final Widget body = switch (context.formFactor) {
FormFactor.desktop => const DesktopShell(),
FormFactor.tablet => const TabletShell(),
FormFactor.mobile => const MobileShell(),
};
return Scaffold(backgroundColor: c.bg, body: body);
return Scaffold(
backgroundColor: c.bg,
body: showBanner
? Column(children: [UpdateBanner(info: info), Expanded(child: body)])
: body,
);
}
}
+6 -10
View File
@@ -47,10 +47,6 @@ class AppUpdateInfo {
/// 更新检查 Notifier。build() 惰性触发:延迟首查 + 每小时轮询。
class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
Timer? _timer;
bool _dismissed = false;
/// 本次会话是否已被用户「稍后」忽略(强制更新不看这个)。
bool get isDismissed => _dismissed;
@override
Future<AppUpdateInfo?> build() async {
@@ -58,18 +54,13 @@ class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
await Future<void>.delayed(_kInitialDelay);
final first = await _check();
_timer = Timer.periodic(_kPollInterval, (_) async {
_dismissed = false; // 新一轮允许再次提示
state = AsyncValue.data(await _check());
});
return first;
}
/// 用户点「稍后」——本次会话内不再自动弹(直到下轮轮询)
void dismiss() => _dismissed = true;
/// 设置页「检查更新」手动触发:立即查一次并回结果(不受 dismiss 影响)。
/// 设置页「检查更新」手动触发:立即查一次并回结果
Future<AppUpdateInfo?> forceCheck() async {
_dismissed = false;
final info = await _check();
state = AsyncValue.data(info);
return info;
@@ -133,6 +124,11 @@ class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
final updateProvider =
AsyncNotifierProvider<UpdateNotifier, AppUpdateInfo?>(UpdateNotifier.new);
/// 用户在更新 banner 点「稍后再说」时忽略的版本号。忽略后 banner 隐藏;出现号
/// 不同的更新版本会重新显示;设置页手动「检查更新」会清空以重新提示。响应式,
/// 供 shell 顶部 banner 的显隐判断。
final dismissedUpdateVersionProvider = StateProvider<String?>((ref) => null);
/// 按当前平台从服务端 download_urls 里取对应下载直链。
String? platformDownloadUrl(Map<String, String> downloadUrls) {
if (Platform.isMacOS) return downloadUrls['macos'];
+59
View File
@@ -6,10 +6,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/update/app_updater.dart';
import '../l10n/app_text.dart';
import '../pangolin_theme.dart';
import '../state/app_providers.dart';
import '../state/update_provider.dart';
import 'pangolin_icons.dart';
@@ -78,3 +80,60 @@ class _UpdateDialog extends StatelessWidget {
);
}
}
/// jiu 式顶部更新横条(非强制更新用;强制更新仍走不可关闭弹窗 showUpdateDialog)。
/// 「立即更新」App 内下载安装;「×」忽略此版本(banner 隐藏,更新版本会重现)。
class UpdateBanner extends ConsumerWidget {
const UpdateBanner({super.key, required this.info});
final AppUpdateInfo info;
@override
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
final t = ref.watch(appTextProvider);
return Material(
color: c.accentSubtle,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 8, 6, 8),
child: Row(children: [
Icon(PangolinIcons.zap, size: 17, color: c.accent),
const SizedBox(width: 10),
Expanded(
child: Text(
t.updateAvailableTitle(info.latestVersion),
style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w700),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => unawaited(startInAppUpdate(context, t, info)),
style: TextButton.styleFrom(
backgroundColor: c.accent,
foregroundColor: c.fgOnAccent,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(PangolinRadius.full)),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(t.lang.updateNow,
style: PangolinText.caption
.copyWith(fontWeight: FontWeight.w700, fontSize: 12.5)),
),
IconButton(
onPressed: () =>
ref.read(dismissedUpdateVersionProvider.notifier).state = info.latestVersion,
icon: Icon(Icons.close, size: 18, color: c.fg3),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
tooltip: t.updateLater,
),
]),
),
),
);
}
}
+15 -1
View File
@@ -51,7 +51,15 @@ VER=""
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
rm -f "$ver_file"
echo "==> compile-windows: tag=${TAG} version=${VER}"
# build number 同 android/macos 规则(MAJOR*10000+MINOR*100+PATCH)。
MAJOR="${VER%%.*}"
_REST="${VER#*.}"
MINOR="${_REST%%.*}"
PATCH="${_REST#*.}"
PATCH="${PATCH%%-*}"
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
echo "==> compile-windows: tag=${TAG} version=${VER} build=${BUILD}"
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
@@ -79,6 +87,12 @@ rm -rf "${BUILD_CLIENT}/windows/flutter/ephemeral" \
"${BUILD_CLIENT}/.dart_tool" \
"${BUILD_CLIENT}/build"
# 同步 Flutter app 版本到 tag(GNU sed,windows runner)。否则 package_info 卡在
# pubspec committed 值(1.0.48),设置页显示旧版 + 自动更新永远判"有新版"。
# 此前只改了 Inno 安装器 MyAppVersion(下方),App 内部版本没跟上——本次修复。
sed -i "s/^version:.*/version: ${VER}+${BUILD}/" "${BUILD_CLIENT}/pubspec.yaml"
echo "==> compile-windows: pubspec version -> ${VER}+${BUILD}"
# ── [3/4] build ──────────────────────────────────────────────────────────────
# NOTE(controller): `flutter create` on an existing project only fills in
# scaffolding it manages (ephemeral/, generated_plugins.cmake, ...) and should