feat: 长辈模式(大字大按钮 + 偏好按账号隔离)+ 语音输入(按住说话 + 实时转写 + 后端代理 DashScope ASR + 患者端专用)+ 健康仪表盘背景渐变

This commit is contained in:
MingNian
2026-07-21 10:53:16 +08:00
parent 0cb5b8e85a
commit 28f704c98e
25 changed files with 2214 additions and 131 deletions

View File

@@ -0,0 +1,162 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_provider.dart';
class ElderModePreferences {
final bool enabled;
final bool largeTextEnabled;
final bool voiceInputEnabled;
const ElderModePreferences({
this.enabled = false,
this.largeTextEnabled = true,
this.voiceInputEnabled = true,
});
ElderModePreferences copyWith({
bool? enabled,
bool? largeTextEnabled,
bool? voiceInputEnabled,
}) {
return ElderModePreferences(
enabled: enabled ?? this.enabled,
largeTextEnabled: largeTextEnabled ?? this.largeTextEnabled,
voiceInputEnabled: voiceInputEnabled ?? this.voiceInputEnabled,
);
}
Map<String, dynamic> toJson() => {
'schemaVersion': 1,
'enabled': enabled,
'largeTextEnabled': largeTextEnabled,
'voiceInputEnabled': voiceInputEnabled,
};
factory ElderModePreferences.fromJson(Map<String, dynamic> json) {
return ElderModePreferences(
enabled: json['enabled'] == true,
largeTextEnabled: json['largeTextEnabled'] != false,
voiceInputEnabled: json['voiceInputEnabled'] != false,
);
}
}
class ElderModeState {
final ElderModePreferences preferences;
final bool isLoaded;
final bool isSaving;
final String? error;
const ElderModeState({
this.preferences = const ElderModePreferences(),
this.isLoaded = false,
this.isSaving = false,
this.error,
});
bool get isEnabled => preferences.enabled;
bool get useLargeText => isEnabled && preferences.largeTextEnabled;
ElderModeState copyWith({
ElderModePreferences? preferences,
bool? isLoaded,
bool? isSaving,
String? error,
bool clearError = false,
}) {
return ElderModeState(
preferences: preferences ?? this.preferences,
isLoaded: isLoaded ?? this.isLoaded,
isSaving: isSaving ?? this.isSaving,
error: clearError ? null : error ?? this.error,
);
}
}
final elderModeProvider = NotifierProvider<ElderModeNotifier, ElderModeState>(
ElderModeNotifier.new,
);
class ElderModeNotifier extends Notifier<ElderModeState> {
static const _storageKeyPrefix = 'accessibility_preferences_v1:user_';
String? _currentUserId;
@override
ElderModeState build() {
final userId = ref.watch(
authProvider.select((state) => state.user?.id),
);
_currentUserId = userId;
if (userId == null || userId.isEmpty) {
return const ElderModeState(isLoaded: true);
}
Future<void>.microtask(_load);
return const ElderModeState();
}
String get _storageKey => '$_storageKeyPrefix$_currentUserId';
Future<void> _load() async {
if (_currentUserId == null || _currentUserId!.isEmpty) return;
try {
final raw = await ref.read(localDbProvider).read(_storageKey);
if (raw == null || raw.trim().isEmpty) {
state = state.copyWith(isLoaded: true, clearError: true);
return;
}
final decoded = jsonDecode(raw);
if (decoded is! Map) throw const FormatException('invalid preferences');
state = state.copyWith(
preferences: ElderModePreferences.fromJson(
Map<String, dynamic>.from(decoded),
),
isLoaded: true,
clearError: true,
);
} catch (_) {
state = state.copyWith(isLoaded: true, error: '长辈模式设置读取失败,已使用默认设置');
}
}
Future<bool> enable() =>
_save(state.preferences.copyWith(enabled: true, largeTextEnabled: true));
Future<bool> disable() => _save(state.preferences.copyWith(enabled: false));
Future<bool> setLargeTextEnabled(bool enabled) =>
_save(state.preferences.copyWith(largeTextEnabled: enabled));
Future<bool> setVoiceInputEnabled(bool enabled) =>
_save(state.preferences.copyWith(voiceInputEnabled: enabled));
Future<bool> _save(ElderModePreferences next) async {
if (_currentUserId == null || _currentUserId!.isEmpty) {
state = state.copyWith(
isLoaded: true,
error: '请先登录后再调整长辈模式',
);
return false;
}
final previous = state.preferences;
state = state.copyWith(
preferences: next,
isLoaded: true,
isSaving: true,
clearError: true,
);
try {
await ref.read(localDbProvider).write(_storageKey, jsonEncode(next));
state = state.copyWith(isSaving: false, clearError: true);
return true;
} catch (_) {
state = state.copyWith(
preferences: previous,
isSaving: false,
error: '保存失败,请稍后重试',
);
return false;
}
}
}