Skip to content

Commit 5de9488

Browse files
committed
fix: 自动修复 SharedPreferences
1 parent ceffc9c commit 5de9488

6 files changed

Lines changed: 180 additions & 2 deletions

File tree

lib/l10n/app_en.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@
461461
"markdownCopyCode": "Copy code",
462462
"markdownCodeCopied": "Code copied to clipboard",
463463
"markdownSpoilerHidden": "Hidden",
464+
"settingsCorruptedResetNotice": "Local settings seem corrupted and have been reset.",
464465

465466
"debugLogs": "Debug Logs",
466467
"debugLogsDescription": "View application logs",

lib/l10n/app_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,6 +2426,12 @@ abstract class AppLocalizations {
24262426
/// **'Hidden'**
24272427
String get markdownSpoilerHidden;
24282428

2429+
/// No description provided for @settingsCorruptedResetNotice.
2430+
///
2431+
/// In en, this message translates to:
2432+
/// **'Local settings seem corrupted and have been reset.'**
2433+
String get settingsCorruptedResetNotice;
2434+
24292435
/// No description provided for @debugLogs.
24302436
///
24312437
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,10 @@ class AppLocalizationsEn extends AppLocalizations {
12351235
@override
12361236
String get markdownSpoilerHidden => 'Hidden';
12371237

1238+
@override
1239+
String get settingsCorruptedResetNotice =>
1240+
'Local settings seem corrupted and have been reset.';
1241+
12381242
@override
12391243
String get debugLogs => 'Debug Logs';
12401244

lib/l10n/app_localizations_zh.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,9 @@ class AppLocalizationsZh extends AppLocalizations {
11911191
@override
11921192
String get markdownSpoilerHidden => '已隐藏';
11931193

1194+
@override
1195+
String get settingsCorruptedResetNotice => '本地设置似乎损坏,已重置';
1196+
11941197
@override
11951198
String get debugLogs => '调试日志';
11961199

lib/l10n/app_zh.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@
432432
"markdownCopyCode": "复制代码",
433433
"markdownCodeCopied": "代码已复制到剪贴板",
434434
"markdownSpoilerHidden": "已隐藏",
435+
"settingsCorruptedResetNotice": "本地设置似乎损坏,已重置",
435436

436437
"debugLogs": "调试日志",
437438
"debugLogsDescription": "查看应用运行日志",

lib/main.dart

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import 'dart:convert';
12
import 'dart:io';
23
import 'package:flutter/foundation.dart' show kIsWeb;
34
import 'package:flutter/material.dart';
45
import 'package:flutter_localizations/flutter_localizations.dart';
6+
import 'package:path_provider/path_provider.dart';
57
import 'package:shared_preferences/shared_preferences.dart';
68
import 'package:window_manager/window_manager.dart';
79
import '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

78205
class 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 {
87219
class _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

Comments
 (0)