Skip to content

Commit f4ff1fd

Browse files
committed
fix: show themed error screen when second instance tries to start
1 parent ce46d2f commit f4ff1fd

File tree

2 files changed

+228
-10
lines changed

2 files changed

+228
-10
lines changed

lib/main.dart

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import 'models/models.dart';
4141
import 'models/node_model.dart';
4242
import 'models/notification_model.dart';
4343
import 'models/trade_wallet_lookup.dart';
44+
import 'pages/already_running_view.dart';
4445
import 'pages/campfire_migrate_view.dart';
4546
import 'pages/home_view/home_view.dart';
4647
import 'pages/intro_view.dart';
@@ -183,22 +184,48 @@ void main(List<String> args) async {
183184
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
184185
} on FileSystemException catch (e) {
185186
if (e.osError?.errorCode == 11 || e.message.contains('lock failed')) {
186-
// Another instance of the app already holds the Hive database lock.
187-
// Show a simple error screen rather than crashing to a black screen.
188-
runApp(
189-
MaterialApp(
187+
// Another instance already holds the Hive database lock.
188+
// Try to bootstrap just enough of the theme system (Isar is independent
189+
// of Hive) so the error screen looks like a real Stack Wallet screen.
190+
Widget errorApp;
191+
try {
192+
await StackFileSystem.initThemesDir();
193+
await MainDB.instance.initMainDB();
194+
ThemeService.instance.init(MainDB.instance);
195+
errorApp = const ProviderScope(child: AlreadyRunningApp());
196+
} catch (_) {
197+
// Isar is also unavailable (e.g., another error). Fall back to a
198+
// minimal but still Inter-font styled screen.
199+
errorApp = MaterialApp(
190200
debugShowCheckedModeBanner: false,
201+
theme: ThemeData(fontFamily: GoogleFonts.inter().fontFamily),
191202
home: Scaffold(
192203
body: Center(
193-
child: Text(
194-
'${AppConfig.appName} is already running.\n'
195-
'Close the other window and try again.',
196-
textAlign: TextAlign.center,
204+
child: Column(
205+
mainAxisSize: MainAxisSize.min,
206+
children: [
207+
Text(
208+
AppConfig.appName,
209+
textAlign: TextAlign.center,
210+
style: GoogleFonts.inter(
211+
fontSize: 20,
212+
fontWeight: FontWeight.w600,
213+
),
214+
),
215+
const SizedBox(height: 8),
216+
Text(
217+
'is already running.\n'
218+
'Close the other window and try again.',
219+
textAlign: TextAlign.center,
220+
style: GoogleFonts.inter(fontSize: 16),
221+
),
222+
],
197223
),
198224
),
199225
),
200-
),
201-
);
226+
);
227+
}
228+
runApp(errorApp);
202229
return;
203230
}
204231
rethrow;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* This file is part of Stack Wallet.
3+
*
4+
* Copyright (c) 2023 Cypher Stack
5+
* All Rights Reserved.
6+
* The code is distributed under GPLv3 license, see LICENSE file for details.
7+
* Generated by Cypher Stack on 2023-05-26
8+
*
9+
*/
10+
11+
import 'dart:io';
12+
13+
import 'package:flutter/material.dart';
14+
import 'package:flutter_riverpod/flutter_riverpod.dart';
15+
import 'package:flutter_svg/svg.dart';
16+
import 'package:google_fonts/google_fonts.dart';
17+
18+
import '../app_config.dart';
19+
import '../themes/stack_colors.dart';
20+
import '../themes/theme_providers.dart';
21+
import '../themes/theme_service.dart';
22+
import '../utilities/stack_file_system.dart';
23+
import '../utilities/text_styles.dart';
24+
import '../utilities/util.dart';
25+
import '../widgets/app_icon.dart';
26+
import '../widgets/background.dart';
27+
28+
/// Root app widget for the "already running" error path.
29+
///
30+
/// Mirrors the theme bootstrap performed by [MaterialAppWithTheme] in main.dart
31+
/// but without touching Hive. Requires Isar + ThemeService to already be
32+
/// initialized before [runApp] is called.
33+
class AlreadyRunningApp extends ConsumerStatefulWidget {
34+
const AlreadyRunningApp({super.key});
35+
36+
@override
37+
ConsumerState<AlreadyRunningApp> createState() => _AlreadyRunningAppState();
38+
}
39+
40+
class _AlreadyRunningAppState extends ConsumerState<AlreadyRunningApp> {
41+
@override
42+
void initState() {
43+
super.initState();
44+
WidgetsBinding.instance.addPostFrameCallback((_) {
45+
ref.read(applicationThemesDirectoryPathProvider.notifier).state =
46+
StackFileSystem.themesDir!.path;
47+
// The first instance already verified/installed the light theme, so
48+
// getTheme cannot return null here.
49+
ref.read(themeProvider.state).state = ref
50+
.read(pThemeService)
51+
.getTheme(themeId: "light")!;
52+
});
53+
}
54+
55+
@override
56+
Widget build(BuildContext context) {
57+
final colorScheme = ref.watch(colorProvider.state).state;
58+
return MaterialApp(
59+
debugShowCheckedModeBanner: false,
60+
title: AppConfig.appName,
61+
theme: ThemeData(
62+
extensions: [colorScheme],
63+
fontFamily: GoogleFonts.inter().fontFamily,
64+
splashColor: Colors.transparent,
65+
),
66+
home: const AlreadyRunningView(),
67+
);
68+
}
69+
}
70+
71+
/// Error screen shown when this is a second instance of the app.
72+
///
73+
/// Mirrors [IntroView]'s layout: themed background, logo, app name heading,
74+
/// short description subtitle, then the error message (in label style, smaller
75+
/// than the subtitle) in place of the action buttons.
76+
class AlreadyRunningView extends ConsumerWidget {
77+
const AlreadyRunningView({super.key});
78+
79+
static const _errorMessage =
80+
"${AppConfig.appName} is already running. "
81+
"Close the other window and try again.";
82+
83+
@override
84+
Widget build(BuildContext context, WidgetRef ref) {
85+
final isDesktop = Util.isDesktop;
86+
final colors = Theme.of(context).extension<StackColors>()!;
87+
final stack = ref.watch(
88+
themeProvider.select((value) => value.assets.stack),
89+
);
90+
91+
return Background(
92+
child: Scaffold(
93+
backgroundColor: colors.background,
94+
body: SafeArea(
95+
child: Center(
96+
child: !isDesktop
97+
? Column(
98+
crossAxisAlignment: CrossAxisAlignment.center,
99+
children: [
100+
const Spacer(flex: 2),
101+
Padding(
102+
padding: const EdgeInsets.symmetric(horizontal: 16),
103+
child: ConstrainedBox(
104+
constraints: const BoxConstraints(maxWidth: 300),
105+
child: SizedBox(
106+
width: 266,
107+
height: 266,
108+
child: stack.endsWith(".png")
109+
? Image.file(File(stack))
110+
: SvgPicture.file(
111+
File(stack),
112+
width: 266,
113+
height: 266,
114+
),
115+
),
116+
),
117+
),
118+
const Spacer(flex: 1),
119+
Text(
120+
AppConfig.appName,
121+
textAlign: TextAlign.center,
122+
style: STextStyles.pageTitleH1(context),
123+
),
124+
const SizedBox(height: 8),
125+
Padding(
126+
padding: const EdgeInsets.symmetric(horizontal: 48),
127+
child: Text(
128+
AppConfig.shortDescriptionText,
129+
textAlign: TextAlign.center,
130+
style: STextStyles.subtitle(context),
131+
),
132+
),
133+
const Spacer(flex: 4),
134+
Padding(
135+
padding: const EdgeInsets.symmetric(
136+
horizontal: 16,
137+
vertical: 16,
138+
),
139+
child: Text(
140+
_errorMessage,
141+
textAlign: TextAlign.center,
142+
style: STextStyles.label(context),
143+
),
144+
),
145+
],
146+
)
147+
: SizedBox(
148+
width: 350,
149+
height: 540,
150+
child: Column(
151+
children: [
152+
const Spacer(flex: 2),
153+
const SizedBox(
154+
width: 130,
155+
height: 130,
156+
child: AppIcon(),
157+
),
158+
const Spacer(flex: 42),
159+
Text(
160+
AppConfig.appName,
161+
textAlign: TextAlign.center,
162+
style: STextStyles.pageTitleH1(
163+
context,
164+
).copyWith(fontSize: 40),
165+
),
166+
const Spacer(flex: 24),
167+
Text(
168+
AppConfig.shortDescriptionText,
169+
textAlign: TextAlign.center,
170+
style: STextStyles.subtitle(
171+
context,
172+
).copyWith(fontSize: 24),
173+
),
174+
const Spacer(flex: 42),
175+
Text(
176+
_errorMessage,
177+
textAlign: TextAlign.center,
178+
style: STextStyles.label(
179+
context,
180+
).copyWith(fontSize: 18),
181+
),
182+
const Spacer(flex: 65),
183+
],
184+
),
185+
),
186+
),
187+
),
188+
),
189+
);
190+
}
191+
}

0 commit comments

Comments
 (0)