bb031a04c1
- 语言(#1):LocaleNotifier 首启无用户选择时读设备 locale(zh/ja/ko/ru/es 命中 对应语种,余回退英文),原来恒为固定默认 en、从不看系统语言。用户显式切换仍持久 化优先。 - 用户中心 SSO(#3):usercenter 运行在 basePath=/user,但 /sso 落地页用原生 window.location.replace('/') 跳转(Next 不补 basePath)→ 落到站点根=官网主页, 且换票已建立的会话看起来像未登录。改为 withBase() 统一补 /user 前缀,兑票成功 直达 /user/ 且保持登录态。(服务端 issue/exchange 流程实测均 200,非后端问题) - 账户页(#4):移除"续费/升级"按钮(免费卡片 + PRO 横幅两处)及其未用组件;购买入口 仍由"购买套餐"行保留,不影响下单路径。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
112 lines
3.9 KiB
Dart
112 lines
3.9 KiB
Dart
// app_providers.dart — 语言 / 主题 / 套餐视角等基础状态(Riverpod)
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../l10n/app_text.dart';
|
|
import '../l10n/strings_en.dart';
|
|
import '../l10n/strings_es.dart';
|
|
import '../l10n/strings_ja.dart';
|
|
import '../l10n/strings_ko.dart';
|
|
import '../l10n/strings_ru.dart';
|
|
import '../l10n/strings_zh.dart';
|
|
import 'account_providers.dart';
|
|
import 'auth_provider.dart';
|
|
|
|
/// AppLang → 文案资源实例。供 [appTextProvider] 与「拿不到 Consumer 的场景」
|
|
/// (model / 非 Consumer 的 tile,只有 AppLang)复用,避免各处重写 switch。
|
|
AppText appTextFor(AppLang lang) {
|
|
switch (lang) {
|
|
case AppLang.zh:
|
|
return const StringsZh();
|
|
case AppLang.en:
|
|
return const StringsEn();
|
|
case AppLang.ja:
|
|
return const StringsJa();
|
|
case AppLang.ko:
|
|
return const StringsKo();
|
|
case AppLang.ru:
|
|
return const StringsRu();
|
|
case AppLang.es:
|
|
return const StringsEs();
|
|
}
|
|
}
|
|
|
|
/// 当前语言。首启无用户选择时**跟随系统语言**(设备 locale → 支持的 AppLang,
|
|
/// 命不中回退英文);用户在设置/账户页显式切换后持久化到 shared_preferences
|
|
/// (key `pg_lang`,存枚举 name),此后一律以用户选择为准、不再看系统。
|
|
class LocaleNotifier extends StateNotifier<AppLang> {
|
|
LocaleNotifier() : super(_systemDefault()) {
|
|
_load();
|
|
}
|
|
static const _key = 'pg_lang';
|
|
|
|
/// 设备语言 → 支持的 AppLang。取平台首选 locale 的语言码(zh/ja/ko/ru/es 命中
|
|
/// 对应语种,其余一律英文)。构造期同步可用,无需 await。
|
|
static AppLang _systemDefault() {
|
|
try {
|
|
final code = ui.PlatformDispatcher.instance.locale.languageCode.toLowerCase();
|
|
for (final l in AppLang.values) {
|
|
if (l.name == code) return l;
|
|
}
|
|
} catch (_) {
|
|
/* 取系统 locale 失败 → 英文 */
|
|
}
|
|
return AppLang.en;
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final saved = (await SharedPreferences.getInstance()).getString(_key);
|
|
if (saved != null) {
|
|
for (final l in AppLang.values) {
|
|
if (l.name == saved) {
|
|
state = l;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (_) {
|
|
/* 读失败保持系统默认 */
|
|
}
|
|
}
|
|
|
|
Future<void> set(AppLang lang) async {
|
|
state = lang;
|
|
try {
|
|
await (await SharedPreferences.getInstance()).setString(_key, lang.name);
|
|
} catch (_) {
|
|
/* 持久化失败忽略,本次会话仍生效 */
|
|
}
|
|
}
|
|
}
|
|
|
|
final localeProvider =
|
|
StateNotifierProvider<LocaleNotifier, AppLang>((ref) => LocaleNotifier());
|
|
|
|
/// 由语言派生的文案资源——UI 一律通过它取文案,不写死字面量。
|
|
final appTextProvider = Provider<AppText>((ref) => appTextFor(ref.watch(localeProvider)));
|
|
|
|
/// 主题模式。默认跟随系统;设置页可显式切深色。
|
|
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
|
|
|
|
/// 是否免费档——派生自真实账户(meProvider.plan == 'free')。
|
|
/// 免费版才显示额度卡与免费横幅;加载中默认按免费处理。
|
|
final isFreePlanProvider = Provider<bool>((ref) {
|
|
final me = ref.watch(meProvider).valueOrNull;
|
|
return me?.isFree ?? true;
|
|
});
|
|
|
|
/// 账户状态是否还没就绪——连接页据此先转圈再渲染,避免「先闪免费广告再跳会员」。
|
|
/// 覆盖两段窗口:① auth 还在从存储读 token(此时 isLoggedIn=false,me 会同步返回
|
|
/// 免费默认,故必须看 auth.isLoading);② 已登录但 me 还在拉真实值(此时 me 可能已有
|
|
/// 过期免费默认值,故看 me.isLoading 而非 !hasValue)。未登录(确定访客)不转圈。
|
|
final meLoadingProvider = Provider<bool>((ref) {
|
|
final auth = ref.watch(authProvider);
|
|
if (auth.isLoading) return true;
|
|
if (!auth.isLoggedIn) return false;
|
|
return ref.watch(meProvider).isLoading;
|
|
});
|