diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 4262583..5f4cd04 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -520,5 +520,6 @@ "selectProvider": "Select a provider", "orderId": "Order ID", "enterOrderId": "Enter your order ID", - "restore": "Restore" + "restore": "Restore", + "keyboardShortcuts": "Keyboard Shortcuts" } diff --git a/lib/main.dart b/lib/main.dart index 26c645c..fa45fbc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,12 +59,31 @@ void main() async { if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) { doWhenWindowReady(() { - const initialSize = Size(360, 640); - appWindow.minSize = initialSize; + const defaultSize = Size(360, 640); + + // Get saved window size from preferences + final savedSizeString = prefs.getString(kAppWindowSize); + Size initialSize = defaultSize; + + if (savedSizeString != null) { + try { + final parts = savedSizeString.split(','); + if (parts.length == 2) { + final width = double.parse(parts[0]); + final height = double.parse(parts[1]); + initialSize = Size(width, height); + } + } catch (e) { + log("[SplashScreen] Failed to parse saved window size: $e"); + initialSize = defaultSize; + } + } + + appWindow.minSize = defaultSize; appWindow.size = initialSize; appWindow.alignment = Alignment.center; appWindow.show(); - log("[SplashScreen] Desktop window is ready!"); + log("[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}"); }); } diff --git a/lib/pods/config.dart b/lib/pods/config.dart index 23b6630..71ef9af 100644 --- a/lib/pods/config.dart +++ b/lib/pods/config.dart @@ -58,6 +58,7 @@ sealed class AppSettings with _$AppSettings { required bool appBarTransparent, required String? customFonts, required int? appColorScheme, // The color stored via the int type + required Size? windowSize, // The window size for desktop platforms }) = _AppSettings; } @@ -74,9 +75,27 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { appBarTransparent: prefs.getBool(kAppbarTransparentStoreKey) ?? false, customFonts: prefs.getString(kAppCustomFonts), appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), + windowSize: _getWindowSizeFromPrefs(prefs), ); } + Size? _getWindowSizeFromPrefs(SharedPreferences prefs) { + final sizeString = prefs.getString(kAppWindowSize); + if (sizeString == null) return null; + + try { + final parts = sizeString.split(','); + if (parts.length == 2) { + final width = double.parse(parts[0]); + final height = double.parse(parts[1]); + return Size(width, height); + } + } catch (e) { + // Invalid format, return null + } + return null; + } + void setAutoTranslate(bool value) { final prefs = ref.read(sharedPreferencesProvider); prefs.setBool(kAppAutoTranslate, value); @@ -121,6 +140,20 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { state = state.copyWith(appColorScheme: value); ref.read(themeProvider.notifier).reloadTheme(); } + + void setWindowSize(Size? size) { + final prefs = ref.read(sharedPreferencesProvider); + if (size != null) { + prefs.setString(kAppWindowSize, '${size.width},${size.height}'); + } else { + prefs.remove(kAppWindowSize); + } + state = state.copyWith(windowSize: size); + } + + Size? getWindowSize() { + return state.windowSize; + } } final updateInfoProvider = diff --git a/lib/pods/config.freezed.dart b/lib/pods/config.freezed.dart index 248183f..74568f0 100644 --- a/lib/pods/config.freezed.dart +++ b/lib/pods/config.freezed.dart @@ -15,7 +15,8 @@ T _$identity(T value) => value; /// @nodoc mixin _$AppSettings { - bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; String? get customFonts; int? get appColorScheme; + bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; String? get customFonts; int? get appColorScheme;// The color stored via the int type + Size? get windowSize; /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,16 +27,16 @@ $AppSettingsCopyWith get copyWith => _$AppSettingsCopyWithImpl Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme); +int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme)'; + return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; } @@ -46,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> { factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; @useResult $Res call({ - bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme + bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme, Size? windowSize }); @@ -63,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { return _then(_self.copyWith( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable @@ -72,7 +73,8 @@ as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ig as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable -as int?, +as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable +as Size?, )); } @@ -83,7 +85,7 @@ as int?, class _AppSettings implements AppSettings { - const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.customFonts, required this.appColorScheme}); + const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.customFonts, required this.appColorScheme, required this.windowSize}); @override final bool autoTranslate; @@ -93,6 +95,8 @@ class _AppSettings implements AppSettings { @override final bool appBarTransparent; @override final String? customFonts; @override final int? appColorScheme; +// The color stored via the int type +@override final Size? windowSize; /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @@ -104,16 +108,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_ @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); } @override -int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme); +int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme)'; + return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; } @@ -124,7 +128,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; @override @useResult $Res call({ - bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme + bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme, Size? windowSize }); @@ -141,7 +145,7 @@ class __$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { return _then(_AppSettings( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable @@ -150,7 +154,8 @@ as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ig as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable -as int?, +as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable +as Size?, )); } diff --git a/lib/pods/config.g.dart b/lib/pods/config.g.dart index d92b563..b8affd2 100644 --- a/lib/pods/config.g.dart +++ b/lib/pods/config.g.dart @@ -7,7 +7,7 @@ part of 'config.dart'; // ************************************************************************** String _$appSettingsNotifierHash() => - r'4f727d448ee17a87b5698b8e36ef67521655406c'; + r'c4f40a3bc4311c6360c2b5e44f8df5e5d7c1bd75'; /// See also [AppSettingsNotifier]. @ProviderFor(AppSettingsNotifier) diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index 1147fe2..cd99a4e 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -122,7 +122,7 @@ class PostComposeScreen extends HookConsumerWidget { context: context, builder: (context) => AlertDialog( - title: Text('keyboard_shortcuts'.tr()), + title: Text('keyboardShortcuts'.tr()), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -221,7 +221,7 @@ class PostComposeScreen extends HookConsumerWidget { ), if (isWideScreen(context)) Tooltip( - message: 'keyboard_shortcuts'.tr(), + message: 'keyboardShortcuts'.tr(), child: IconButton( icon: const Icon(Symbols.keyboard), onPressed: showKeyboardShortcutsDialog, diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index a67d0dd..e4c4ae8 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -99,7 +99,7 @@ class ArticleComposeScreen extends HookConsumerWidget { context: context, builder: (context) => AlertDialog( - title: Text('keyboard_shortcuts'.tr()), + title: Text('keyboardShortcuts'.tr()), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -281,11 +281,21 @@ class ArticleComposeScreen extends HookConsumerWidget { item: attachments[idx], progress: progressMap[idx], onRequestUpload: - () => ComposeLogic.uploadAttachment(ref, state, idx), + () => ComposeLogic.uploadAttachment( + ref, + state, + idx, + ), onDelete: - () => ComposeLogic.deleteAttachment(ref, state, idx), + () => ComposeLogic.deleteAttachment( + ref, + state, + idx, + ), onMove: (delta) { - state.attachments.value = ComposeLogic.moveAttachment( + state + .attachments + .value = ComposeLogic.moveAttachment( state.attachments.value, idx, delta, @@ -325,7 +335,7 @@ class ArticleComposeScreen extends HookConsumerWidget { ), if (isWideScreen(context)) Tooltip( - message: 'keyboard_shortcuts'.tr(), + message: 'keyboardShortcuts'.tr(), child: IconButton( icon: const Icon(Symbols.keyboard), onPressed: showKeyboardShortcutsDialog, diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 685830d..0955626 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -5,7 +5,9 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/config.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; @@ -22,6 +24,26 @@ class WindowScaffold extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // Add window resize listener for desktop platforms + useEffect(() { + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { + void saveWindowSize() { + final size = appWindow.size; + final settingsNotifier = ref.read(appSettingsNotifierProvider.notifier); + settingsNotifier.setWindowSize(size); + } + + // Save window size when app is about to close + WidgetsBinding.instance.addObserver(_WindowSizeObserver(saveWindowSize)); + + return () { + // Cleanup observer when widget is disposed + WidgetsBinding.instance.removeObserver(_WindowSizeObserver(saveWindowSize)); + }; + } + return null; + }, []); + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; @@ -101,6 +123,34 @@ class WindowScaffold extends HookConsumerWidget { } } +class _WindowSizeObserver extends WidgetsBindingObserver { + final VoidCallback onSaveWindowSize; + + _WindowSizeObserver(this.onSaveWindowSize); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + // Save window size when app is paused, detached, or hidden + if (state == AppLifecycleState.paused || + state == AppLifecycleState.detached || + state == AppLifecycleState.hidden) { + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { + onSaveWindowSize(); + } + } + } + + @override + bool operator ==(Object other) { + return other is _WindowSizeObserver && other.onSaveWindowSize == onSaveWindowSize; + } + + @override + int get hashCode => onSaveWindowSize.hashCode; +} + final rootScaffoldKey = GlobalKey(); class AppScaffold extends StatelessWidget {