mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
514 lines
16 KiB
Dart
514 lines
16 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:common/constants.dart';
|
|
import 'package:common/model/device.dart';
|
|
import 'package:common/model/stored_security_context.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:localsend_app/gen/strings.g.dart';
|
|
import 'package:localsend_app/model/persistence/color_mode.dart';
|
|
import 'package:localsend_app/model/persistence/favorite_device.dart';
|
|
import 'package:localsend_app/model/persistence/receive_history_entry.dart';
|
|
import 'package:localsend_app/model/send_mode.dart';
|
|
import 'package:localsend_app/provider/window_dimensions_provider.dart';
|
|
import 'package:localsend_app/util/alias_generator.dart';
|
|
import 'package:localsend_app/util/native/autostart_helper.dart';
|
|
import 'package:localsend_app/util/native/context_menu_helper.dart';
|
|
import 'package:localsend_app/util/native/platform_check.dart';
|
|
import 'package:localsend_app/util/security_helper.dart';
|
|
import 'package:localsend_app/util/shared_preferences/shared_preferences_file.dart';
|
|
import 'package:localsend_app/util/shared_preferences/shared_preferences_portable.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:refena_flutter/refena_flutter.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
part 'persistence_provider_migrations.dart';
|
|
|
|
final _logger = Logger('PersistenceService');
|
|
|
|
String get _windowsFile {
|
|
final appData = Platform.environment['APPDATA'];
|
|
return '$appData\\LocalSend\\settings.json';
|
|
}
|
|
|
|
String get _windowsLegacyFile {
|
|
final appData = Platform.environment['APPDATA'];
|
|
return '$appData\\org.localsend\\localsend_app\\shared_preferences.json';
|
|
}
|
|
|
|
// Version of the storage
|
|
const _version = 'ls_version';
|
|
|
|
// Security keys (generated on first app start)
|
|
const _securityContext = 'ls_security_context';
|
|
|
|
// Received file history
|
|
const _receiveHistory = 'ls_receive_history';
|
|
|
|
// Favorites
|
|
const _favorites = 'ls_favorites';
|
|
|
|
// App Window Offset and Size info
|
|
const _windowOffsetX = 'ls_window_offset_x';
|
|
const _windowOffsetY = 'ls_window_offset_y';
|
|
const _windowWidth = 'ls_window_width';
|
|
const _windowHeight = 'ls_window_height';
|
|
const _saveWindowPlacement = 'ls_save_window_placement';
|
|
|
|
// Settings
|
|
const _showToken = 'ls_show_token';
|
|
const _aliasKey = 'ls_alias';
|
|
const _themeKey = 'ls_theme'; // now called brightness
|
|
const _colorKey = 'ls_color';
|
|
const _localeKey = 'ls_locale';
|
|
const _portKey = 'ls_port';
|
|
const _networkWhitelistKey = 'ls_network_whitelist';
|
|
const _networkBlacklistKey = 'ls_network_blacklist';
|
|
const _timeoutKey = 'ls_timeout';
|
|
const _multicastGroupKey = 'ls_multicast_group';
|
|
const _destinationKey = 'ls_destination';
|
|
const _saveToGallery = 'ls_save_to_gallery';
|
|
const _saveToHistory = 'ls_save_to_history';
|
|
const _quickSave = 'ls_quick_save';
|
|
const _quickSaveFromFavorites = 'ls_quick_save_from_favorites';
|
|
const _receivePin = 'ls_receive_pin';
|
|
const _autoFinish = 'ls_auto_finish';
|
|
const _minimizeToTray = 'ls_minimize_to_tray';
|
|
const _https = 'ls_https';
|
|
const _sendMode = 'ls_send_mode';
|
|
const _enableAnimations = 'ls_enable_animations';
|
|
const _deviceType = 'ls_device_type';
|
|
const _deviceModel = 'ls_device_model';
|
|
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
|
|
const _advancedSettingsKey = 'ls_advanced_settings';
|
|
|
|
final persistenceProvider = Provider<PersistenceService>((ref) {
|
|
throw Exception('persistenceProvider not initialized');
|
|
});
|
|
|
|
/// This service abstracts the persistence layer.
|
|
class PersistenceService {
|
|
final SharedPreferences _prefs;
|
|
final bool isFirstAppStart;
|
|
|
|
PersistenceService._(this._prefs, this.isFirstAppStart);
|
|
|
|
static Future<PersistenceService> initialize({
|
|
required bool supportsDynamicColors,
|
|
}) async {
|
|
SharedPreferences prefs;
|
|
|
|
final portableStore = SharedPreferencesPortable();
|
|
bool usingLegacyStore = false;
|
|
if (checkPlatform(const [TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.macOS]) && portableStore.exists()) {
|
|
_logger.info('Using portable settings.');
|
|
SharedPreferencesStorePlatform.instance = portableStore;
|
|
} else if (defaultTargetPlatform == TargetPlatform.windows) {
|
|
final legacyStore = SharedPreferencesFile(filePath: _windowsLegacyFile);
|
|
if (legacyStore.exists()) {
|
|
_logger.info('Using legacy settings. Will migrate in the next step.');
|
|
SharedPreferencesStorePlatform.instance = legacyStore;
|
|
usingLegacyStore = true;
|
|
} else {
|
|
SharedPreferencesStorePlatform.instance = SharedPreferencesFile(filePath: _windowsFile);
|
|
}
|
|
}
|
|
|
|
final bool isFirstAppStart;
|
|
final existingVersion = (await SharedPreferencesStorePlatform.instance.getAll())['flutter.$_version'] as int?;
|
|
_logger.info('Existing version: $existingVersion');
|
|
if (existingVersion == null && !usingLegacyStore) {
|
|
isFirstAppStart = true;
|
|
await SharedPreferencesStorePlatform.instance.setValue('Int', 'flutter.$_version', _latestVersion);
|
|
} else {
|
|
isFirstAppStart = false;
|
|
final fromVersion = existingVersion ?? 1;
|
|
if (fromVersion < _latestVersion) {
|
|
await _runMigrations(fromVersion);
|
|
}
|
|
}
|
|
|
|
try {
|
|
prefs = await SharedPreferences.getInstance();
|
|
} catch (e) {
|
|
if (checkPlatform([TargetPlatform.windows])) {
|
|
_logger.info('Could not initialize SharedPreferences, trying to delete corrupted settings file', e);
|
|
File(_windowsFile).deleteSync();
|
|
prefs = await SharedPreferences.getInstance();
|
|
} else {
|
|
throw Exception('Could not initialize SharedPreferences');
|
|
}
|
|
}
|
|
|
|
// Locale configuration upon persistence initialisation to prevent unlocalised Alias generation
|
|
final persistedLocale = prefs.getString(_localeKey);
|
|
if (persistedLocale == null) {
|
|
await LocaleSettings.useDeviceLocale();
|
|
} else {
|
|
await LocaleSettings.setLocaleRaw(persistedLocale);
|
|
}
|
|
|
|
if (prefs.getString(_showToken) == null) {
|
|
await prefs.setString(_showToken, const Uuid().v4());
|
|
}
|
|
|
|
if (prefs.getString(_aliasKey) == null) {
|
|
await prefs.setString(_aliasKey, generateRandomAlias());
|
|
}
|
|
|
|
if (prefs.getString(_securityContext) == null) {
|
|
await prefs.setString(_securityContext, jsonEncode(generateSecurityContext()));
|
|
}
|
|
|
|
if (prefs.getString(_colorKey) == null) {
|
|
await _initColorSetting(prefs, supportsDynamicColors);
|
|
} else {
|
|
// fix when device does not support dynamic colors
|
|
final supported = supportsDynamicColors ? ColorMode.values : ColorMode.values.where((e) => e != ColorMode.system);
|
|
final colorMode = supported.firstWhereOrNull((color) => color.name == prefs.getString(_colorKey));
|
|
if (colorMode == null) {
|
|
await _initColorSetting(prefs, supportsDynamicColors);
|
|
}
|
|
}
|
|
|
|
// migrate legacy auto start settings (current implementation is stateless and relies on the Windows registry / file system)
|
|
const launchAtStartupLegacyKey = 'ls_launch_at_startup';
|
|
const launchMinimizedLegacyKey = 'ls_auto_start_launch_minimized';
|
|
if (prefs.getBool(launchAtStartupLegacyKey) == true) {
|
|
_logger.info('Enable auto start on legacy settings');
|
|
await prefs.remove(launchAtStartupLegacyKey);
|
|
await enableAutoStart(startHidden: prefs.getBool(launchMinimizedLegacyKey) == true);
|
|
await prefs.remove(launchMinimizedLegacyKey);
|
|
}
|
|
|
|
return PersistenceService._(prefs, isFirstAppStart);
|
|
}
|
|
|
|
static Future<void> _initColorSetting(SharedPreferences prefs, bool supportsDynamicColors) async {
|
|
await prefs.setString(
|
|
_colorKey,
|
|
checkPlatform([TargetPlatform.android]) && supportsDynamicColors ? ColorMode.system.name : ColorMode.localsend.name,
|
|
);
|
|
}
|
|
|
|
bool isPortableMode() {
|
|
return SharedPreferencesStorePlatform.instance is SharedPreferencesPortable;
|
|
}
|
|
|
|
StoredSecurityContext getSecurityContext() {
|
|
final contextRaw = _prefs.getString(_securityContext)!;
|
|
return StoredSecurityContext.fromJson(jsonDecode(contextRaw));
|
|
}
|
|
|
|
Future<void> setSecurityContext(StoredSecurityContext context) async {
|
|
await _prefs.setString(_securityContext, jsonEncode(context));
|
|
}
|
|
|
|
List<ReceiveHistoryEntry> getReceiveHistory() {
|
|
final historyRaw = _prefs.getStringList(_receiveHistory) ?? [];
|
|
return historyRaw.map((entry) => ReceiveHistoryEntry.fromJson(jsonDecode(entry))).toList();
|
|
}
|
|
|
|
Future<void> setReceiveHistory(List<ReceiveHistoryEntry> entries) async {
|
|
final historyRaw = entries.map((entry) => jsonEncode(entry.toJson())).toList();
|
|
await _prefs.setStringList(_receiveHistory, historyRaw);
|
|
}
|
|
|
|
List<FavoriteDevice> getFavorites() {
|
|
final favoritesRaw = _prefs.getStringList(_favorites) ?? [];
|
|
return favoritesRaw.map((entry) => FavoriteDevice.fromJson(jsonDecode(entry))).toList();
|
|
}
|
|
|
|
Future<void> setFavorites(List<FavoriteDevice> entries) async {
|
|
final favoritesRaw = entries.map((entry) => jsonEncode(entry.toJson())).toList();
|
|
await _prefs.setStringList(_favorites, favoritesRaw);
|
|
}
|
|
|
|
String getShowToken() {
|
|
return _prefs.getString(_showToken)!;
|
|
}
|
|
|
|
String getAlias() {
|
|
return _prefs.getString(_aliasKey) ?? generateRandomAlias();
|
|
}
|
|
|
|
Future<void> setAlias(String alias) async {
|
|
await _prefs.setString(_aliasKey, alias);
|
|
}
|
|
|
|
ThemeMode getTheme() {
|
|
final value = _prefs.getString(_themeKey);
|
|
if (value == null) {
|
|
return ThemeMode.system;
|
|
}
|
|
return ThemeMode.values.firstWhereOrNull((theme) => theme.name == value) ?? ThemeMode.system;
|
|
}
|
|
|
|
Future<void> setTheme(ThemeMode theme) async {
|
|
await _prefs.setString(_themeKey, theme.name);
|
|
}
|
|
|
|
ColorMode getColorMode() {
|
|
final value = _prefs.getString(_colorKey);
|
|
if (value == null) {
|
|
return ColorMode.system;
|
|
}
|
|
return ColorMode.values.firstWhereOrNull((color) => color.name == value) ?? ColorMode.system;
|
|
}
|
|
|
|
Future<void> setColorMode(ColorMode color) async {
|
|
await _prefs.setString(_colorKey, color.name);
|
|
}
|
|
|
|
AppLocale? getLocale() {
|
|
final value = _prefs.getString(_localeKey);
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
return AppLocale.values.firstWhereOrNull((locale) => locale.languageTag == value);
|
|
}
|
|
|
|
Future<void> setLocale(AppLocale? locale) async {
|
|
if (locale == null) {
|
|
await _prefs.remove(_localeKey);
|
|
} else {
|
|
await _prefs.setString(_localeKey, locale.languageTag);
|
|
}
|
|
}
|
|
|
|
int getPort() {
|
|
return _prefs.getInt(_portKey) ?? defaultPort;
|
|
}
|
|
|
|
Future<void> setPort(int port) async {
|
|
await _prefs.setInt(_portKey, port);
|
|
}
|
|
|
|
List<String>? getNetworkWhitelist() {
|
|
return _prefs.getStringList(_networkWhitelistKey);
|
|
}
|
|
|
|
Future<void> setNetworkWhitelist(List<String>? whitelist) async {
|
|
if (whitelist == null) {
|
|
await _prefs.remove(_networkWhitelistKey);
|
|
} else {
|
|
await _prefs.setStringList(_networkWhitelistKey, whitelist);
|
|
}
|
|
}
|
|
|
|
List<String>? getNetworkBlacklist() {
|
|
return _prefs.getStringList(_networkBlacklistKey);
|
|
}
|
|
|
|
Future<void> setNetworkBlacklist(List<String>? blacklist) async {
|
|
if (blacklist == null) {
|
|
await _prefs.remove(_networkBlacklistKey);
|
|
} else {
|
|
await _prefs.setStringList(_networkBlacklistKey, blacklist);
|
|
}
|
|
}
|
|
|
|
int getDiscoveryTimeout() {
|
|
return _prefs.getInt(_timeoutKey) ?? defaultDiscoveryTimeout;
|
|
}
|
|
|
|
Future<void> setDiscoveryTimeout(int timeout) async {
|
|
await _prefs.setInt(_timeoutKey, timeout);
|
|
}
|
|
|
|
bool getShareViaLinkAutoAccept() {
|
|
return _prefs.getBool(_shareViaLinkAutoAccept) ?? false;
|
|
}
|
|
|
|
Future<void> setShareViaLinkAutoAccept(bool shareViaLinkAutoAccept) async {
|
|
await _prefs.setBool(_shareViaLinkAutoAccept, shareViaLinkAutoAccept);
|
|
}
|
|
|
|
String getMulticastGroup() {
|
|
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
|
|
}
|
|
|
|
Future<void> setMulticastGroup(String group) async {
|
|
await _prefs.setString(_multicastGroupKey, group);
|
|
}
|
|
|
|
String? getDestination() {
|
|
return _prefs.getString(_destinationKey);
|
|
}
|
|
|
|
Future<void> setDestination(String? destination) async {
|
|
if (destination == null) {
|
|
await _prefs.remove(_destinationKey);
|
|
} else {
|
|
await _prefs.setString(_destinationKey, destination);
|
|
}
|
|
}
|
|
|
|
bool isSaveToGallery() {
|
|
return _prefs.getBool(_saveToGallery) ?? true;
|
|
}
|
|
|
|
Future<void> setSaveToGallery(bool saveToGallery) async {
|
|
await _prefs.setBool(_saveToGallery, saveToGallery);
|
|
}
|
|
|
|
bool isSaveToHistory() {
|
|
return _prefs.getBool(_saveToHistory) ?? true;
|
|
}
|
|
|
|
Future<void> setSaveToHistory(bool saveToHistory) async {
|
|
await _prefs.setBool(_saveToHistory, saveToHistory);
|
|
}
|
|
|
|
bool getAdvancedSettingsEnabled() {
|
|
return _prefs.getBool(_advancedSettingsKey) ?? false;
|
|
}
|
|
|
|
Future<void> setAdvancedSettingsEnabled(bool isEnabled) async {
|
|
await _prefs.setBool(_advancedSettingsKey, isEnabled);
|
|
}
|
|
|
|
bool isQuickSave() {
|
|
return _prefs.getBool(_quickSave) ?? false;
|
|
}
|
|
|
|
Future<void> setQuickSave(bool quickSave) async {
|
|
await _prefs.setBool(_quickSave, quickSave);
|
|
}
|
|
|
|
bool isQuickSaveFromFavorites() {
|
|
return _prefs.getBool(_quickSaveFromFavorites) ?? false;
|
|
}
|
|
|
|
Future<void> setQuickSaveFromFavorites(bool quickSaveFromFavorites) async {
|
|
await _prefs.setBool(_quickSaveFromFavorites, quickSaveFromFavorites);
|
|
}
|
|
|
|
String? getReceivePin() {
|
|
return _prefs.getString(_receivePin);
|
|
}
|
|
|
|
Future<void> setReceivePin(String? pin) async {
|
|
if (pin == null) {
|
|
await _prefs.remove(_receivePin);
|
|
} else {
|
|
await _prefs.setString(_receivePin, pin);
|
|
}
|
|
}
|
|
|
|
bool isAutoFinish() {
|
|
return _prefs.getBool(_autoFinish) ?? false;
|
|
}
|
|
|
|
Future<void> setAutoFinish(bool autoFinish) async {
|
|
await _prefs.setBool(_autoFinish, autoFinish);
|
|
}
|
|
|
|
bool isMinimizeToTray() {
|
|
return _prefs.getBool(_minimizeToTray) ?? false;
|
|
}
|
|
|
|
Future<void> setMinimizeToTray(bool minimizeToTray) async {
|
|
await _prefs.setBool(_minimizeToTray, minimizeToTray);
|
|
}
|
|
|
|
bool isHttps() {
|
|
return _prefs.getBool(_https) ?? true;
|
|
}
|
|
|
|
Future<void> setHttps(bool https) async {
|
|
await _prefs.setBool(_https, https);
|
|
}
|
|
|
|
SendMode getSendMode() {
|
|
return SendMode.values.firstWhereOrNull((m) => m.name == _prefs.getString(_sendMode)) ?? SendMode.single;
|
|
}
|
|
|
|
Future<void> setSendMode(SendMode mode) async {
|
|
await _prefs.setString(_sendMode, mode.name);
|
|
}
|
|
|
|
Future<void> setWindowOffsetX(double x) async {
|
|
await _prefs.setDouble(_windowOffsetX, x);
|
|
}
|
|
|
|
Future<void> setWindowOffsetY(double y) async {
|
|
await _prefs.setDouble(_windowOffsetY, y);
|
|
}
|
|
|
|
Future<void> setWindowHeight(double height) async {
|
|
await _prefs.setDouble(_windowHeight, height);
|
|
}
|
|
|
|
Future<void> setWindowWidth(double width) async {
|
|
await _prefs.setDouble(_windowWidth, width);
|
|
}
|
|
|
|
WindowDimensions? getWindowLastDimensions() {
|
|
Size? size;
|
|
Offset? position;
|
|
final offsetX = _prefs.getDouble(_windowOffsetX);
|
|
final offsetY = _prefs.getDouble(_windowOffsetY);
|
|
final width = _prefs.getDouble(_windowWidth);
|
|
final height = _prefs.getDouble(_windowHeight);
|
|
|
|
if (width != null && height != null) {
|
|
size = Size(width, height);
|
|
}
|
|
|
|
if (offsetX != null && offsetY != null) {
|
|
position = Offset(offsetX, offsetY);
|
|
}
|
|
|
|
if (size == null || position == null) {
|
|
return null;
|
|
}
|
|
|
|
return WindowDimensions(
|
|
position: position,
|
|
size: size,
|
|
);
|
|
}
|
|
|
|
Future<void> setSaveWindowPlacement(bool savePlacement) async {
|
|
await _prefs.setBool(_saveWindowPlacement, savePlacement);
|
|
}
|
|
|
|
bool getSaveWindowPlacement() {
|
|
if (!checkPlatformIsNotWaylandDesktop()) return false;
|
|
return _prefs.getBool(_saveWindowPlacement) ?? true;
|
|
}
|
|
|
|
Future<void> setEnableAnimations(bool enableAnimations) async {
|
|
await _prefs.setBool(_enableAnimations, enableAnimations);
|
|
}
|
|
|
|
bool getEnableAnimations() {
|
|
return _prefs.getBool(_enableAnimations) ?? true;
|
|
}
|
|
|
|
DeviceType? getDeviceType() {
|
|
return DeviceType.values.firstWhereOrNull((m) => m.name == _prefs.getString(_deviceType));
|
|
}
|
|
|
|
Future<void> setDeviceType(DeviceType deviceType) async {
|
|
await _prefs.setString(_deviceType, deviceType.name);
|
|
}
|
|
|
|
String? getDeviceModel() {
|
|
return _prefs.getString(_deviceModel);
|
|
}
|
|
|
|
Future<void> setDeviceModel(String deviceModel) async {
|
|
await _prefs.setString(_deviceModel, deviceModel);
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
await _prefs.clear();
|
|
}
|
|
}
|