Files
2026-08-30 21:25:22 +08:00

97 lines
3.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 全局昵称设置
// 持久化到 SharedPreferences / localStorage,所有工具共享同一个昵称
// Web 平台自动监听 localStorage 变化 → 跨标签页实时同步
import 'dart:async';
import 'dart:html' as html show window, StorageEvent;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:shared_preferences/shared_preferences.dart';
const _keyNickname = 'toolbox.nickname';
const _keyBrowserId = 'toolbox.browser_id';
class NicknameService {
NicknameService._();
static final NicknameService instance = NicknameService._();
SharedPreferences? _prefs;
/// 浏览器唯一 ID:同浏览器跨 tab 共享(localStorage 作用域),跨浏览器不同
/// 用于后端识别"同一个人",避免昵称冲突时自动改名
String? _browserId;
/// 昵称变化广播流:本地 setNickname 触发 + Web 上其他标签页修改也触发
final _streamController = StreamController<String>.broadcast();
Stream<String> get onChanged => _streamController.stream;
StreamSubscription? _webStorageSub;
Future<void> _ensureInit() async {
_prefs ??= await SharedPreferences.getInstance();
// 惰性生成 browserId:首次访问时若没有就生成 uuid 存起来
if (_browserId == null) {
final existing = _prefs!.getString(_keyBrowserId);
if (existing != null && existing.isNotEmpty) {
_browserId = existing;
} else {
_browserId = _genBrowserId();
await _prefs!.setString(_keyBrowserId, _browserId!);
}
}
}
/// 获取浏览器 ID(稳定值,同浏览器跨 tab 相同)
Future<String> get browserId async {
await _ensureInit();
return _browserId!;
}
/// 生成短 browserId:base62(0-9A-Za-z),去掉特殊字符方便 URL 使用
/// 12 字符足够唯一(62^12 ≈ 3e21 种可能)
String _genBrowserId() {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
final rand = DateTime.now().microsecond.hashCode;
final buf = StringBuffer();
int seed = rand ^ DateTime.now().millisecondsSinceEpoch;
for (int i = 0; i < 12; i++) {
seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF;
buf.write(chars[seed % chars.length]);
}
return buf.toString();
}
/// 开启跨标签页同步(仅 Web 生效;非 Web 空操作)
/// 在 widget 的 initState 里调用一次即可
void enableCrossTabSync() {
if (!kIsWeb || _webStorageSub != null) return;
_webStorageSub = html.window.onStorage.listen((html.StorageEvent event) async {
if (event.key == _keyNickname) {
final n = await nickname;
_streamController.add(n);
}
});
}
/// 获取当前昵称,未设置时返回默认昵称
Future<String> get nickname async {
await _ensureInit();
final n = _prefs!.getString(_keyNickname);
if (n != null && n.trim().isNotEmpty) return n.trim();
return '用户';
}
/// 设置昵称(1-32 字符,过短返回 false)
Future<bool> setNickname(String value) async {
final trimmed = value.trim();
if (trimmed.isEmpty || trimmed.length > 32) return false;
await _ensureInit();
await _prefs!.setString(_keyNickname, trimmed);
_streamController.add(trimmed);
return true;
}
void dispose() {
_webStorageSub?.cancel();
_webStorageSub = null;
}
}