1+ import 'dart:convert' ;
12import 'dart:io' ;
23import 'package:flutter/foundation.dart' show kIsWeb;
34import 'package:flutter/material.dart' ;
45import 'package:flutter_localizations/flutter_localizations.dart' ;
6+ import 'package:path_provider/path_provider.dart' ;
57import 'package:shared_preferences/shared_preferences.dart' ;
68import 'package:window_manager/window_manager.dart' ;
79import 'package:media_kit/media_kit.dart' ;
@@ -17,6 +19,7 @@ void main() async {
1719 MediaKit .ensureInitialized ();
1820
1921 final isDesktop = ! kIsWeb && (Platform .isWindows || Platform .isMacOS || Platform .isLinux);
22+ final startupRecovery = await _performStartupRecovery (isDesktop: isDesktop);
2023 await SettingsService .instance.init ();
2124
2225 if (isDesktop) {
@@ -72,13 +75,142 @@ void main() async {
7275 talker.error ('AuthState.init error' , e);
7376 });
7477
75- runApp (TouchFishApp (isFirstLaunch: isFirstLaunch));
78+ runApp (
79+ TouchFishApp (
80+ isFirstLaunch: isFirstLaunch,
81+ didResetLocalSettings: startupRecovery.didResetSharedPreferences,
82+ ),
83+ );
84+ }
85+
86+ Future <_StartupRecoveryResult > _performStartupRecovery ({
87+ required bool isDesktop,
88+ }) async {
89+ final didResetSharedPreferences = isDesktop
90+ ? await _repairSharedPreferencesFileIfCorrupted ()
91+ : false ;
92+
93+ final prefs = await SharedPreferences .getInstance ();
94+ final didResetWindowPosition = isDesktop
95+ ? await _resetWindowPositionIfFarOutsideScreen (prefs)
96+ : false ;
97+
98+ return _StartupRecoveryResult (
99+ didResetSharedPreferences: didResetSharedPreferences,
100+ didResetWindowPosition: didResetWindowPosition,
101+ );
102+ }
103+
104+ Future <bool > _repairSharedPreferencesFileIfCorrupted () async {
105+ try {
106+ final supportDirectory = await getApplicationSupportDirectory ();
107+ final preferencesFile = File (
108+ '${supportDirectory .path }${Platform .pathSeparator }shared_preferences.json' ,
109+ );
110+
111+ if (! await preferencesFile.exists ()) {
112+ return false ;
113+ }
114+
115+ final rawText = await preferencesFile.readAsString ();
116+ if (rawText.trim ().isEmpty) {
117+ return false ;
118+ }
119+
120+ final decoded = jsonDecode (rawText);
121+ if (decoded is Map ) {
122+ return false ;
123+ }
124+
125+ await preferencesFile.writeAsString ('{}' , flush: true );
126+ talker.warning ('Shared preferences file had an invalid root JSON value and was reset.' );
127+ return true ;
128+ } on FormatException catch (error, stackTrace) {
129+ try {
130+ final supportDirectory = await getApplicationSupportDirectory ();
131+ final preferencesFile = File (
132+ '${supportDirectory .path }${Platform .pathSeparator }shared_preferences.json' ,
133+ );
134+ await preferencesFile.writeAsString ('{}' , flush: true );
135+ } catch (writeError, writeStackTrace) {
136+ talker.error ('Failed to rewrite corrupted shared preferences file.' , writeError, writeStackTrace);
137+ return false ;
138+ }
139+
140+ talker.error ('Shared preferences JSON parse failed and the file was reset.' , error, stackTrace);
141+ return true ;
142+ } catch (error, stackTrace) {
143+ talker.error ('Failed while checking shared preferences file integrity.' , error, stackTrace);
144+ return false ;
145+ }
146+ }
147+
148+ Future <bool > _resetWindowPositionIfFarOutsideScreen (
149+ SharedPreferences prefs,
150+ ) async {
151+ final savedX = prefs.getDouble ('window_x' );
152+ final savedY = prefs.getDouble ('window_y' );
153+
154+ if (savedX == null || savedY == null ) {
155+ return false ;
156+ }
157+
158+ final views = WidgetsBinding .instance.platformDispatcher.views;
159+ if (views.isEmpty) {
160+ return false ;
161+ }
162+
163+ final display = views.first.display;
164+ final devicePixelRatio = display.devicePixelRatio == 0
165+ ? 1.0
166+ : display.devicePixelRatio;
167+ final screenWidth = display.size.width / devicePixelRatio;
168+ final screenHeight = display.size.height / devicePixelRatio;
169+ final savedWidth = prefs.getDouble ('window_width' ) ?? 1280 ;
170+ final savedHeight = prefs.getDouble ('window_height' ) ?? 800 ;
171+
172+ final allowedBounds = Rect .fromLTWH (
173+ - screenWidth,
174+ - screenHeight,
175+ screenWidth * 3 ,
176+ screenHeight * 3 ,
177+ );
178+ final windowBounds = Rect .fromLTWH (
179+ savedX,
180+ savedY,
181+ savedWidth,
182+ savedHeight,
183+ );
184+
185+ if (windowBounds.overlaps (allowedBounds)) {
186+ return false ;
187+ }
188+
189+ await prefs.remove ('window_x' );
190+ await prefs.remove ('window_y' );
191+ talker.warning ('Saved window position was far outside the current screen bounds and was reset.' );
192+ return true ;
193+ }
194+
195+ class _StartupRecoveryResult {
196+ final bool didResetSharedPreferences;
197+ final bool didResetWindowPosition;
198+
199+ const _StartupRecoveryResult ({
200+ required this .didResetSharedPreferences,
201+ required this .didResetWindowPosition,
202+ });
76203}
77204
78205class TouchFishApp extends StatefulWidget {
79206 final bool isFirstLaunch;
207+ final bool didResetLocalSettings;
80208
81- const TouchFishApp ({super .key, required this .isFirstLaunch});
209+ const TouchFishApp ({
210+ super .key,
211+ required this .isFirstLaunch,
212+ this .didResetLocalSettings = false ,
213+ });
82214
83215 @override
84216 State <TouchFishApp > createState () => _TouchFishAppState ();
@@ -87,6 +219,36 @@ class TouchFishApp extends StatefulWidget {
87219class _TouchFishAppState extends State <TouchFishApp > {
88220 final _appState = AppState .instance;
89221 late final _router = AppRoutes .createRouter (isFirstLaunch: widget.isFirstLaunch);
222+ bool _didShowStartupResetNotice = false ;
223+
224+ void _showStartupResetNoticeIfNeeded (BuildContext context) {
225+ if (_didShowStartupResetNotice || ! widget.didResetLocalSettings) {
226+ return ;
227+ }
228+
229+ _didShowStartupResetNotice = true ;
230+ WidgetsBinding .instance.addPostFrameCallback ((_) {
231+ if (! mounted) return ;
232+
233+ final l10n = AppLocalizations .of (context);
234+ if (l10n == null ) return ;
235+
236+ showDialog <void >(
237+ context: context,
238+ builder: (dialogContext) {
239+ return AlertDialog (
240+ content: Text (l10n.settingsCorruptedResetNotice),
241+ actions: [
242+ TextButton (
243+ onPressed: () => Navigator .of (dialogContext).pop (),
244+ child: Text (MaterialLocalizations .of (dialogContext).okButtonLabel),
245+ ),
246+ ],
247+ );
248+ },
249+ );
250+ });
251+ }
90252
91253 @override
92254 Widget build (BuildContext context) {
@@ -149,6 +311,7 @@ class _TouchFishAppState extends State<TouchFishApp> {
149311 ),
150312 themeMode: _appState.themeMode,
151313 builder: (context, child) {
314+ _showStartupResetNoticeIfNeeded (context);
152315 if (hasBackgroundImage && ! kIsWeb) {
153316 return Container (
154317 color: Theme .of (context).colorScheme.surface,
0 commit comments