Files
jwinterm 04203654f1 Rename Flutter project + binary: cake_wallet -> hash_wallet
The big one. Touches 760+ files. Locks in the user's decision to break
import-line compatibility with upstream cake-tech/cake_wallet in exchange
for a binary named hash_wallet on every platform.

Changes:
- pubspec_description.yaml: project name cake_wallet -> hash_wallet
- 762 Dart files: every 'package:cake_wallet/...' import rewritten to
  'package:hash_wallet/...' (lib/, cw_*/lib/, tool/, test/, integration_test/)
- linux/CMakeLists.txt: BINARY_NAME cake_wallet -> hash_wallet
- windows/CMakeLists.txt: project + BINARY_NAME (CakeWallet -> HashWallet)
- windows/runner/Runner.rc: company / file / product strings to Hash Wallet
  + Such Software LLC copyright
- macos/Runner.xcodeproj/project_base.pbxproj: cake_wallet_new.app refs
  -> hash_wallet.app (macOS output bundle name)
- com.suchsoftware.HashWallet.yml (Flatpak): all binary path refs
  cake_wallet -> hash_wallet (e.g. /app/hash_wallet/hash_wallet)
- linux/com.suchsoftware.HashWallet.desktop: Exec=hash_wallet
- scripts/android/copy_monero_deps.sh: CW_DIR target path
- configure_hash_wallet.sh: error message typo (old filename)

INTENTIONALLY UNCHANGED:
- cw_* subpackage names (cw_core, cw_bitcoin, cw_wownero, etc.) — they
  remain as the historical 'cake wallet' naming; renaming them would
  cascade through every sub-package's internal structure
- Method channel name 'com.cake_wallet/native_utils' in iOS/macOS/Android
  AppDelegates and MainActivity — it's a string identifier that has to
  match the Dart side exactly. Renaming requires both-sides change; it's
  internal-only and not user-visible.
- ghcr.io/cake-tech/cake_wallet:debian13-... docker image references in
  build scripts — that's Cake Labs' published image we use as the build
  environment, not ours.

User data directory now follows the new binary name:
  ~/.local/share/hash_wallet/  (was ~/.local/share/cake_wallet/)
  ~/.config/hash_wallet/       (was ~/.config/cake_wallet/)
Existing users on the old name will need migration if any exist.
2026-05-16 11:33:25 -04:00

289 lines
8.3 KiB
Dart

import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/utils/device_info.dart';
import 'package:hash_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/themes/utils/theme_list.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'material_base_theme.dart';
import 'package:hash_wallet/themes/theme_classes/black_theme.dart';
part 'theme_store.g.dart';
/// MobX store for managing theme state
class ThemeStore = ThemeStoreBase with _$ThemeStore;
abstract class ThemeStoreBase with Store {
@observable
MaterialThemeBase _currentTheme = ThemeList.lightTheme;
@observable
ThemeMode _themeMode = ThemeMode.system;
@observable
bool _isOled = false;
@computed
MaterialThemeBase get currentTheme => _currentTheme;
@computed
ThemeMode get themeMode => _themeMode;
@computed
bool get isDarkMode => _currentTheme.isDark;
@computed
bool get isOled => _isOled;
@computed
bool get hasCustomTheme => sharedPreferences.getInt(PreferencesKey.currentTheme) != null;
@computed
MaterialThemeBase? get savedCustomTheme {
final raw = sharedPreferences.getInt(PreferencesKey.currentTheme);
return raw != null ? ThemeList.deserialize(raw: raw) : null;
}
@computed
MaterialThemeBase? get savedDarkTheme {
try {
final raw = sharedPreferences.getInt(PreferencesKey.savedDarkTheme);
return raw != null ? ThemeList.deserialize(raw: raw) : null;
} catch (e) {
return null;
}
}
@computed
MaterialThemeBase? get savedLightTheme {
try {
final raw = sharedPreferences.getInt(PreferencesKey.savedLightTheme);
return raw != null ? ThemeList.deserialize(raw: raw) : null;
} catch (e) {
return null;
}
}
late SharedPreferences sharedPreferences;
@action
Future<void> setTheme(MaterialThemeBase theme) async {
if (_currentTheme == theme) return;
_currentTheme = theme;
await sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw);
_isOled = theme is BlackTheme ? theme.isOled : false;
await sharedPreferences.setBool(PreferencesKey.blackThemeOled, _isOled);
// Save preferred themes for system mode
if (theme.isDark) {
await sharedPreferences.setInt(PreferencesKey.savedDarkTheme, theme.raw);
} else {
await sharedPreferences.setInt(PreferencesKey.savedLightTheme, theme.raw);
}
}
@action
Future<void> setThemeMode(ThemeMode mode) async {
if (_themeMode == mode) return;
_themeMode = mode;
await _saveThemeModeToPrefs(mode);
if (mode == ThemeMode.system) {
// We'll always use getThemeFromSystem() for system mode to respect saved themes
await setTheme(getThemeFromSystem());
return;
}
if (!hasCustomTheme) return;
final savedTheme = savedCustomTheme;
if (savedTheme == null) return;
if (_isThemeCompatibleWithMode(savedTheme, mode)) {
await setTheme(savedTheme);
}
}
@action
Future<void> setOledEnabled(bool value) async {
if (_isOled == value) return;
_isOled = value;
await sharedPreferences.setBool(PreferencesKey.blackThemeOled, value);
if (_currentTheme is BlackTheme) {
final current = _currentTheme as BlackTheme;
await setTheme(BlackTheme(current.accentColor, isOled: value));
}
}
Future<void> _saveThemeModeToPrefs(ThemeMode mode) async {
await sharedPreferences.setString(PreferencesKey.themeMode, mode.toString());
}
/// Loads the saved theme preferences
Future<void> loadThemePreferences() async {
sharedPreferences = await SharedPreferences.getInstance();
await _loadThemeMode();
await _loadAndSetTheme();
_setupThemeReaction();
}
/// Loads the saved theme mode from SharedPreferences
Future<void> _loadThemeMode() async {
final savedThemeMode = sharedPreferences.getString(PreferencesKey.themeMode);
if (savedThemeMode != null) {
_themeMode = ThemeMode.values.firstWhere(
(mode) => mode.toString() == savedThemeMode,
orElse: () => ThemeMode.system,
);
}
}
/// Loads and sets the appropriate theme based on platform and installation status
Future<void> _loadAndSetTheme() async {
final bool isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? true;
bool shouldUseMobileTheme =
responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile;
if (shouldUseMobileTheme) {
await _handleMobileTheme(isNewInstall);
} else {
await _handleNonMobileTheme();
}
}
/// Handles theme loading for non-mobile platforms
Future<void> _handleNonMobileTheme() async {
await setTheme(ThemeList.darkTheme);
await setThemeMode(ThemeMode.dark);
}
/// Handles theme loading for mobile platforms
Future<void> _handleMobileTheme(bool isNewInstall) async {
if (isNewInstall) {
await _setSystemTheme(isNewInstall: isNewInstall);
} else {
await loadSavedTheme();
}
}
/// Loads the saved theme from SharedPreferences
Future<void> loadSavedTheme({bool isFromBackup = false}) async {
try {
_isOled = sharedPreferences.getBool(PreferencesKey.blackThemeOled) ?? false;
final theme = savedCustomTheme;
if (!hasCustomTheme || theme == null) {
await _setSystemTheme();
return;
}
if (theme is BlackTheme) {
final adjusted = BlackTheme(theme.accentColor, isOled: _isOled);
if (_currentTheme != adjusted) {
await setTheme(adjusted);
}
} else if (_currentTheme != theme) {
await setTheme(theme);
}
final newThemeMode = _getThemeModeOnStartUp(theme, isFromBackup);
if (newThemeMode == ThemeMode.system) {
await _setSystemTheme();
return;
}
if (_themeMode != newThemeMode) {
await setThemeMode(newThemeMode);
}
} catch (e) {
await _setSystemTheme();
}
}
Future<void> _setSystemTheme({bool isNewInstall = false}) async {
if (!isNewInstall && hasCustomTheme) return;
final systemTheme = getThemeFromSystem();
if (_currentTheme != systemTheme) {
await setTheme(systemTheme);
}
if (isNewInstall) {
await _saveThemeModeToPrefs(ThemeMode.system);
if (_themeMode != ThemeMode.system) {
await setThemeMode(ThemeMode.system);
}
}
}
ThemeMode _getThemeModeOnStartUp(MaterialThemeBase theme, bool isFromBackup) {
if (isFromBackup || _themeMode != ThemeMode.system) {
return theme.isDark ? ThemeMode.dark : ThemeMode.light;
}
return ThemeMode.system;
}
void _setupThemeReaction() {
reaction(
(_) => currentTheme,
(MaterialThemeBase theme) => sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw),
);
}
MaterialThemeBase getThemeFromSystem() {
final systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
if (systemBrightness == Brightness.dark) {
// Prefer saved dark theme when available
try {
final savedDark = savedDarkTheme;
if (savedDark != null) {
if (savedDark is BlackTheme) {
// We'll use the OLED flag from the saved theme itself, not from separate preference
return BlackTheme(savedDark.accentColor, isOled: savedDark.isOled);
}
return savedDark;
}
} catch (_) {}
// If no saved dark theme, check if current theme is BlackTheme with accent colors
// and preserve it when switching to dark mode
if (_currentTheme is BlackTheme) {
final currentBlackTheme = _currentTheme as BlackTheme;
return BlackTheme(currentBlackTheme.accentColor, isOled: currentBlackTheme.isOled);
}
return ThemeList.darkTheme;
}
// Light system preference: prefer saved light theme when available
try {
final savedLight = savedLightTheme;
if (savedLight != null) return savedLight;
} catch (_) {}
return ThemeList.lightTheme;
}
/// Checks if a theme is compatible with a theme mode
bool _isThemeCompatibleWithMode(MaterialThemeBase theme, ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return !theme.isDark;
case ThemeMode.dark:
return theme.isDark;
case ThemeMode.system:
return true; // All themes are compatible with system mode
}
}
}