~repos /only-bible-app

#kotlin#android#ios

git clone https://pyrossh.dev/repos/only-bible-app.git

The only bible app you will ever need. No ads. No in-app purchases. No distractions.


f5abd3f1 pyrossh

2 years ago
refactor state management
lib/app.dart CHANGED
@@ -2,15 +2,13 @@ import "package:flutter/material.dart";
2
2
  import "package:flutter_gen/gen_l10n/app_localizations.dart";
3
3
  import "package:only_bible_app/screens/bible_select_screen.dart";
4
4
  import "package:only_bible_app/screens/chapter_view_screen.dart";
5
+ import "package:only_bible_app/state.dart";
5
6
  import "package:only_bible_app/theme.dart";
6
7
  import "package:only_bible_app/utils.dart";
7
8
  import "package:only_bible_app/widgets/scaffold_markdown.dart";
8
9
 
9
10
  class App extends StatelessWidget {
10
- final int initialBook;
11
+ const App({super.key});
11
- final int initialChapter;
12
-
13
- const App({super.key, required this.initialBook, required this.initialChapter});
14
12
 
15
13
  @override
16
14
  Widget build(BuildContext context) {
@@ -19,10 +17,10 @@ class App extends StatelessWidget {
19
17
  localizationsDelegates: AppLocalizations.localizationsDelegates,
20
18
  supportedLocales: AppLocalizations.supportedLocales,
21
19
  debugShowCheckedModeBanner: false,
22
- themeMode: context.app.darkMode ? ThemeMode.dark : ThemeMode.light,
20
+ themeMode: darkMode.watch(context) ? ThemeMode.dark : ThemeMode.light,
23
21
  theme: lightTheme,
24
22
  darkTheme: darkTheme,
25
- locale: context.app.locale,
23
+ locale: Locale(languageCode.watch(context)),
26
24
  // initialRoute: "",
27
25
  routes: {
28
26
  // TODO: maybe have a landing page
@@ -30,9 +28,12 @@ class App extends StatelessWidget {
30
28
  "/privacy-policy": (context) => const ScaffoldMarkdown(title: "Privacy Policy", file: "privacy-policy.md"),
31
29
  "/about-us": (context) => const ScaffoldMarkdown(title: "About Us", file: "about-us.md"),
32
30
  },
33
- home: context.app.firstOpen
31
+ home: firstOpen.value
34
32
  ? const BibleSelectScreen()
33
+ : ChapterViewScreen(
34
+ bookIndex: savedBook.value,
35
- : ChapterViewScreen(bookIndex: initialBook, chapterIndex: initialChapter),
35
+ chapterIndex: savedChapter.value,
36
+ ),
36
37
  );
37
38
  }
38
39
  }
lib/atom.dart ADDED
@@ -0,0 +1,73 @@
1
+ import "package:flutter/material.dart";
2
+ import "package:flutter/scheduler.dart";
3
+ import "package:get_storage/get_storage.dart";
4
+
5
+ late GetStorage localBox;
6
+
7
+ ensureAtomsInitialized(GetStorage box) async {
8
+ localBox = box;
9
+ await localBox.initStorage;
10
+ }
11
+
12
+ class Atom<T> extends ValueNotifier<T> {
13
+ final String key;
14
+ final bool persist;
15
+ Function()? set;
16
+ Function(T)? update;
17
+
18
+ Atom({required this.key, required T initialValue, this.persist = true, this.set, this.update})
19
+ : super(persist ? localBox.read<T>(key) ?? initialValue : initialValue);
20
+
21
+ @override
22
+ set value(T newValue) {
23
+ super.value = newValue;
24
+ if (persist) {
25
+ if (newValue == null) {
26
+ localBox.remove(key);
27
+ } else {
28
+ localBox.write(key, newValue);
29
+ }
30
+ localBox.save();
31
+ }
32
+ }
33
+
34
+ T watch(BuildContext context) {
35
+ final elementRef = WeakReference(context as Element);
36
+ final listenerWrapper = _ListenerWrapper();
37
+ listenerWrapper.listener = () {
38
+ assert(
39
+ SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks,
40
+ """
41
+ Do not mutate state (by setting the value of the ValueNotifier
42
+ that you are subscribed to) during a `build` method. If you need
43
+ to schedule a value update after `build` has completed, use
44
+ `SchedulerBinding.instance.scheduleTask(updateTask, Priority.idle)`,
45
+ `SchedulerBinding.addPostFrameCallback(updateTask)`, '
46
+ or similar.
47
+ """,
48
+ );
49
+ // If the element has not been garbage collected (causing
50
+ // `elementRef.target` to be null), or unmounted
51
+ if (elementRef.target?.mounted ?? false) {
52
+ // Mark the element as needing to be rebuilt
53
+ elementRef.target!.markNeedsBuild();
54
+ }
55
+ // Remove the listener -- only listen to one change per `build`
56
+ removeListener(listenerWrapper.listener!);
57
+ };
58
+ addListener(listenerWrapper.listener!);
59
+ return value;
60
+ }
61
+
62
+ /// Use this method to notify listeners of deeper changes, e.g. when a value
63
+ /// is added to or removed from a set which is stored in the value of a
64
+ /// `ValueNotifier<Set<T>>`.
65
+ void notifyChanged() {
66
+ // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
67
+ notifyListeners();
68
+ }
69
+ }
70
+
71
+ class _ListenerWrapper {
72
+ void Function()? listener;
73
+ }
lib/main.dart CHANGED
@@ -6,8 +6,8 @@ import "package:firebase_crashlytics/firebase_crashlytics.dart";
6
6
  import "package:only_bible_app/firebase_options.dart";
7
7
  import "package:flutter_native_splash/flutter_native_splash.dart";
8
8
  import "package:only_bible_app/app.dart";
9
- import "package:only_bible_app/providers/app_provider.dart";
9
+ import "package:only_bible_app/navigation.dart";
10
- import "package:provider/provider.dart";
10
+ import "package:only_bible_app/state.dart";
11
11
 
12
12
  void main() async {
13
13
  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
@@ -24,14 +24,9 @@ void main() async {
24
24
  };
25
25
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
26
26
  usePathUrlStrategy();
27
+ await initState();
27
- final model = AppProvider();
28
+ updateStatusBar(darkMode.value);
28
- await model.loadData();
29
+ await loadBible();
29
- final (book, chapter) = model.loadBookChapter();
30
- runApp(
30
+ runApp(const App());
31
- ChangeNotifierProvider.value(
32
- value: model,
33
- child: App(initialBook: book, initialChapter: chapter),
34
- ),
35
- );
36
31
  FlutterNativeSplash.remove();
37
32
  }
lib/models.dart CHANGED
@@ -55,9 +55,10 @@ class Book {
55
55
 
56
56
  class Chapter {
57
57
  final int index;
58
+ final int book;
58
59
  final List<Verse> verses;
59
60
 
60
- const Chapter({required this.index, required this.verses});
61
+ const Chapter({required this.index, required this.verses, required this.book});
61
62
  }
62
63
 
63
64
  class Verse {
@@ -90,8 +91,13 @@ List<Book> getBibleFromText(String text) {
90
91
  );
91
92
  }
92
93
  if (books[book - 1].chapters.length < chapter) {
93
- // ignore: prefer_const_constructors
94
- books[book - 1].chapters.add(Chapter(index: chapter - 1, verses: []));
94
+ books[book - 1].chapters.add(
95
+ Chapter(
96
+ index: chapter - 1,
97
+ book: book - 1,
98
+ verses: [],
99
+ ),
100
+ );
95
101
  }
96
102
  books[book - 1].chapters[chapter - 1].verses.add(
97
103
  Verse(
@@ -104,3 +110,10 @@ List<Book> getBibleFromText(String text) {
104
110
  }
105
111
  return books;
106
112
  }
113
+
114
+ class HistoryFrame {
115
+ final int book;
116
+ final int chapter;
117
+
118
+ const HistoryFrame({required this.book, required this.chapter});
119
+ }
lib/navigation.dart ADDED
@@ -0,0 +1,223 @@
1
+ import "package:flutter/material.dart";
2
+ import "package:flutter/services.dart";
3
+ import "package:only_bible_app/models.dart";
4
+ import "package:only_bible_app/screens/bible_select_screen.dart";
5
+ import "package:only_bible_app/screens/book_select_screen.dart";
6
+ import "package:only_bible_app/screens/chapter_view_screen.dart";
7
+ import "package:only_bible_app/sheets/actions_sheet.dart";
8
+ import "package:only_bible_app/sheets/highlight_sheet.dart";
9
+ import "package:only_bible_app/sheets/settings_sheet.dart";
10
+ import "package:only_bible_app/state.dart";
11
+ import "package:only_bible_app/utils.dart";
12
+ import "package:only_bible_app/widgets/scaffold_markdown.dart";
13
+ import "package:share_plus/share_plus.dart";
14
+ import "package:only_bible_app/atom.dart";
15
+
16
+ final Atom<bool> actionsShown = Atom<bool>(
17
+ key: "actionsShown",
18
+ initialValue: false,
19
+ persist: false,
20
+ update: (bool v) {
21
+ actionsShown.value = v;
22
+ },
23
+ );
24
+
25
+ final Atom<bool> highlightsShown = Atom<bool>(
26
+ key: "highlightsShown",
27
+ initialValue: false,
28
+ persist: false,
29
+ update: (bool v) {
30
+ highlightsShown.value = v;
31
+ },
32
+ );
33
+
34
+ updateStatusBar(bool v) {
35
+ if (v) {
36
+ SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
37
+ systemNavigationBarColor: Color(0xFF1F1F22),
38
+ statusBarColor: Color(0xFF1F1F22),
39
+ systemNavigationBarIconBrightness: Brightness.light,
40
+ statusBarIconBrightness: Brightness.light,
41
+ ));
42
+ } else {
43
+ SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
44
+ systemNavigationBarColor: Colors.white,
45
+ statusBarColor: Colors.white,
46
+ systemNavigationBarIconBrightness: Brightness.dark,
47
+ statusBarIconBrightness: Brightness.dark,
48
+ ));
49
+ }
50
+ }
51
+
52
+ pushBookChapter(BuildContext context, int book, int chapter, TextDirection? dir) {
53
+ savedBook.update!(book);
54
+ savedChapter.update!(chapter);
55
+ clearEvents(context);
56
+ Navigator.of(context).push(
57
+ createSlideRoute(
58
+ context: context,
59
+ slideDir: dir,
60
+ page: ChapterViewScreen(bookIndex: book, chapterIndex: chapter),
61
+ ),
62
+ );
63
+ }
64
+
65
+ replaceBookChapter(BuildContext context, int book, int chapter) {
66
+ savedBook.update!(book);
67
+ savedChapter.update!(chapter);
68
+ clearEvents(context);
69
+ Navigator.of(context).pushReplacement(
70
+ createNoTransitionPageRoute(
71
+ ChapterViewScreen(bookIndex: book, chapterIndex: chapter),
72
+ ),
73
+ );
74
+ }
75
+
76
+ nextChapter(BuildContext context, Bible bible, int book, int chapter) {
77
+ final selectedBook = bible.books[book];
78
+ if (selectedBook.chapters.length > chapter + 1) {
79
+ pushBookChapter(context, selectedBook.index, chapter + 1, TextDirection.ltr);
80
+ } else {
81
+ if (selectedBook.index + 1 < bible.books.length) {
82
+ final nextBook = bible.books[selectedBook.index + 1];
83
+ pushBookChapter(context, nextBook.index, 0, TextDirection.ltr);
84
+ }
85
+ }
86
+ }
87
+
88
+ previousChapter(BuildContext context, Bible bible, int book, int chapter) {
89
+ final selectedBook = bible.books[book];
90
+ if (chapter - 1 >= 0) {
91
+ // if (Navigator.of(context).canPop()) {
92
+ // Navigator.of(context).pop();
93
+ // } else {
94
+ pushBookChapter(context, selectedBook.index, chapter - 1, TextDirection.rtl);
95
+ // }
96
+ } else {
97
+ if (selectedBook.index - 1 >= 0) {
98
+ final prevBook = bible.books[selectedBook.index - 1];
99
+ pushBookChapter(context, prevBook.index, prevBook.chapters.length - 1, TextDirection.rtl);
100
+ }
101
+ }
102
+ }
103
+
104
+ showAboutUs(BuildContext context) {
105
+ Navigator.of(context).push(
106
+ createNoTransitionPageRoute(
107
+ const ScaffoldMarkdown(title: "About Us", file: "about-us.md"),
108
+ ),
109
+ );
110
+ }
111
+
112
+ showPrivacyPolicy(BuildContext context) {
113
+ Navigator.of(context).push(
114
+ createNoTransitionPageRoute(
115
+ const ScaffoldMarkdown(title: "Privacy Policy", file: "privacy-policy.md"),
116
+ ),
117
+ );
118
+ }
119
+
120
+ changeBible(BuildContext context) {
121
+ Navigator.of(context).pushReplacement(
122
+ createNoTransitionPageRoute(
123
+ const BibleSelectScreen(),
124
+ ),
125
+ );
126
+ }
127
+
128
+ changeBibleFromHeader(BuildContext context) {
129
+ Navigator.of(context).push(
130
+ createNoTransitionPageRoute(
131
+ const BibleSelectScreen(),
132
+ ),
133
+ );
134
+ }
135
+
136
+ changeBook(BuildContext context, Bible bible) {
137
+ Navigator.of(context).push(
138
+ createNoTransitionPageRoute(
139
+ BookSelectScreen(bible: bible),
140
+ ),
141
+ );
142
+ }
143
+
144
+ shareAppLink(BuildContext context) {
145
+ if (isAndroid()) {
146
+ Share.share(
147
+ subject: "Only Bible App",
148
+ "https://play.google.com/store/apps/details?id=packageName",
149
+ );
150
+ } else if (isIOS()) {
151
+ Share.share(
152
+ subject: "Only Bible App",
153
+ "https://apps.apple.com/us/app/hare-pro/id123",
154
+ );
155
+ } else {
156
+ Share.share(
157
+ subject: "Only Bible App",
158
+ "https://onlybible.app",
159
+ );
160
+ }
161
+ }
162
+
163
+ rateApp(BuildContext context) {
164
+ if (isAndroid()) {
165
+ openUrl(context, "https://play.google.com/store/apps/details?id=packageName");
166
+ } else if (isIOS()) {
167
+ openUrl(context, "https://apps.apple.com/us/app/only-bible-app/packageName");
168
+ }
169
+ }
170
+
171
+ shareVerses(BuildContext context, Bible bible, List<Verse> verses) {
172
+ final name = bible.books[verses.first.book].name(context);
173
+ final chapter = verses.first.chapter + 1;
174
+ final title = "$name $chapter: ${verses.map((e) => e.index + 1).join(", ")}";
175
+ final text = verses.map((e) => e.text).join("\n");
176
+ Share.share("$title\n$text", subject: title);
177
+ }
178
+
179
+ showSettings(BuildContext context) {
180
+ showModalBottomSheet(
181
+ context: context,
182
+ isDismissible: true,
183
+ enableDrag: true,
184
+ showDragHandle: true,
185
+ useSafeArea: true,
186
+ builder: (context) => const SettingsSheet(),
187
+ );
188
+ }
189
+
190
+ showActions(BuildContext context) {
191
+ if (!actionsShown.value) {
192
+ actionsShown.value = true;
193
+ Scaffold.of(context).showBottomSheet(
194
+ enableDrag: false,
195
+ clipBehavior: Clip.antiAliasWithSaveLayer,
196
+ (context) => const ActionsSheet(),
197
+ );
198
+ }
199
+ }
200
+
201
+ hideActions(BuildContext context) {
202
+ if (actionsShown.value) {
203
+ actionsShown.value = false;
204
+ clearSelections();
205
+ Navigator.of(context).pop();
206
+ }
207
+ }
208
+
209
+ showHighlights(BuildContext context) {
210
+ highlightsShown.value = true;
211
+ Scaffold.of(context).showBottomSheet(
212
+ enableDrag: false,
213
+ clipBehavior: Clip.antiAliasWithSaveLayer,
214
+ (context) => const HighlightSheet(),
215
+ );
216
+ }
217
+
218
+ hideHighlights(BuildContext context) {
219
+ if (highlightsShown.value) {
220
+ highlightsShown.value = false;
221
+ Navigator.of(context).pop();
222
+ }
223
+ }
lib/providers/app_provider.dart DELETED
@@ -1,538 +0,0 @@
1
- // import "package:firebase_performance/firebase_performance.dart";
2
- import "dart:developer";
3
- import "package:firebase_crashlytics/firebase_crashlytics.dart";
4
- import "package:firebase_storage/firebase_storage.dart";
5
- import "package:just_audio/just_audio.dart";
6
- import "package:only_bible_app/dialog.dart";
7
- import "package:only_bible_app/screens/chapter_view_screen.dart";
8
- import "package:only_bible_app/theme.dart";
9
- import "package:share_plus/share_plus.dart";
10
- import "package:flutter/material.dart";
11
- import "package:flutter/services.dart";
12
- import "package:only_bible_app/screens/bible_select_screen.dart";
13
- import "package:only_bible_app/screens/book_select_screen.dart";
14
- import "package:only_bible_app/models.dart";
15
- import "package:only_bible_app/sheets/actions_sheet.dart";
16
- import "package:only_bible_app/sheets/highlight_sheet.dart";
17
- import "package:only_bible_app/widgets/scaffold_markdown.dart";
18
- import "package:only_bible_app/widgets/note_sheet.dart";
19
- import "package:only_bible_app/sheets/settings_sheet.dart";
20
- import "package:package_info_plus/package_info_plus.dart";
21
- import "package:provider/provider.dart";
22
- import "package:shared_preferences/shared_preferences.dart";
23
- import "package:get_storage/get_storage.dart";
24
- import "package:only_bible_app/utils.dart";
25
-
26
- class HistoryFrame {
27
- final int book;
28
- final int chapter;
29
- final int? verse;
30
-
31
- const HistoryFrame({required this.book, required this.chapter, this.verse});
32
- }
33
-
34
- class AppProvider extends ChangeNotifier {
35
- late PackageInfo packageInfo;
36
- late Bible bible;
37
- late Locale locale;
38
- bool engTitles = false;
39
- bool darkMode = false;
40
- bool fontBold = false;
41
- double textScaleFactor = 0;
42
- bool actionsShown = false;
43
- bool highlightsShown = false;
44
- final player = AudioPlayer();
45
- bool isPlaying = false;
46
- final List<Verse> selectedVerses = [];
47
- final TextEditingController noteTextController = TextEditingController();
48
- List<HistoryFrame> history = [];
49
- final box = GetStorage("only-bible-app-backup");
50
-
51
- get firstOpen => box.read("firstOpen") ?? true;
52
-
53
- set firstOpen(v) {
54
- box.write("firstOpen", v);
55
- box.save();
56
- }
57
-
58
- static AppProvider of(BuildContext context) {
59
- return Provider.of(context, listen: true);
60
- }
61
-
62
- static AppProvider ofEvent(BuildContext context) {
63
- return Provider.of(context, listen: false);
64
- }
65
-
66
- save() async {
67
- final prefs = await SharedPreferences.getInstance();
68
- await prefs.setString("bibleName", bible.name);
69
- await prefs.setBool("engTitles", engTitles);
70
- await prefs.setBool("darkMode", darkMode);
71
- await prefs.setBool("fontBold", fontBold);
72
- await prefs.setDouble("textScaleFactor", textScaleFactor);
73
- await prefs.setString("languageCode", locale.languageCode);
74
- }
75
-
76
- loadData() async {
77
- packageInfo = await PackageInfo.fromPlatform();
78
- final prefs = await SharedPreferences.getInstance();
79
- engTitles = prefs.getBool("engTitles") ?? false;
80
- darkMode = prefs.getBool("darkMode") ?? false;
81
- fontBold = prefs.getBool("fontBold") ?? false;
82
- textScaleFactor = prefs.getDouble("textScaleFactor") ?? 1;
83
- locale = Locale(prefs.getString("languageCode") ?? "en");
84
- bible = await loadBible(prefs.getString("bibleName") ?? "English");
85
- // await Future.delayed(Duration(seconds: 3));
86
- updateStatusBar();
87
- }
88
-
89
- Future<Bible> loadBible(String name) async {
90
- // Trace customTrace;
91
- // if (!isDesktop()) {
92
- // customTrace = FirebasePerformance.instance.newTrace("loadBible");
93
- // await customTrace.start();
94
- // }
95
- final books = await getBibleFromAsset(name);
96
- // if (!isDesktop()) {
97
- // await customTrace.stop();
98
- // }
99
- return Bible.withBooks(
100
- name: name,
101
- books: books,
102
- );
103
- }
104
-
105
- hasAudio(BuildContext context) {
106
- return context.l.hasAudio == "true";
107
- }
108
-
109
- changeBible(BuildContext context) {
110
- Navigator.of(context).pushReplacement(
111
- createNoTransitionPageRoute(
112
- const BibleSelectScreen(),
113
- ),
114
- );
115
- }
116
-
117
- changeBibleFromHeader(BuildContext context) {
118
- Navigator.of(context).push(
119
- createNoTransitionPageRoute(
120
- const BibleSelectScreen(),
121
- ),
122
- );
123
- }
124
-
125
- // TODO: maybe don't pass name here
126
- updateCurrentBible(BuildContext context, Locale l, String name) async {
127
- // TODO: maybe use a future as the bible needs to load
128
- locale = l;
129
- bible = await loadBible(name);
130
- notifyListeners();
131
- save();
132
- }
133
-
134
- changeBook(BuildContext context) {
135
- Navigator.of(context).push(
136
- createNoTransitionPageRoute(
137
- BookSelectScreen(bible: bible),
138
- ),
139
- );
140
- }
141
-
142
- clearEvents(BuildContext context) {
143
- // if (isPlaying) {
144
- // pause();
145
- // }
146
- clearSelections();
147
- hideActions(context);
148
- }
149
-
150
- onNext(BuildContext context, int book, int chapter) {
151
- final selectedBook = bible.books[book];
152
- if (selectedBook.chapters.length > chapter + 1) {
153
- pushBookChapter(context, selectedBook.index, chapter + 1, TextDirection.ltr);
154
- } else {
155
- if (selectedBook.index + 1 < bible.books.length) {
156
- final nextBook = bible.books[selectedBook.index + 1];
157
- pushBookChapter(context, nextBook.index, 0, TextDirection.ltr);
158
- }
159
- }
160
- }
161
-
162
- onPrevious(BuildContext context, int book, int chapter) {
163
- final selectedBook = bible.books[book];
164
- if (chapter - 1 >= 0) {
165
- // if (Navigator.of(context).canPop()) {
166
- // Navigator.of(context).pop();
167
- // } else {
168
- pushBookChapter(context, selectedBook.index, chapter - 1, TextDirection.rtl);
169
- // }
170
- } else {
171
- if (selectedBook.index - 1 >= 0) {
172
- final prevBook = bible.books[selectedBook.index - 1];
173
- pushBookChapter(context, prevBook.index, prevBook.chapters.length - 1, TextDirection.rtl);
174
- }
175
- }
176
- }
177
-
178
- (int, int) loadBookChapter() {
179
- return (box.read("book") ?? 0, box.read("chapter") ?? 0);
180
- }
181
-
182
- saveBookChapter(int book, int chapter) {
183
- box.write("book", book);
184
- box.write("chapter", chapter);
185
- box.save();
186
- }
187
-
188
- pushBookChapter(BuildContext context, int book, int chapter, TextDirection? dir) {
189
- saveBookChapter(book, chapter);
190
- clearEvents(context);
191
- Navigator.of(context).push(
192
- createSlideRoute(
193
- context: context,
194
- slideDir: dir,
195
- page: ChapterViewScreen(bookIndex: book, chapterIndex: chapter),
196
- ),
197
- );
198
- }
199
-
200
- replaceBookChapter(BuildContext context, int book, int chapter) {
201
- saveBookChapter(book, chapter);
202
- clearEvents(context);
203
- Navigator.of(context).pushReplacement(
204
- createNoTransitionPageRoute(
205
- ChapterViewScreen(bookIndex: book, chapterIndex: chapter),
206
- ),
207
- );
208
- }
209
-
210
- updateFirstOpen() {
211
- box.write("firstOpen", true);
212
- box.save();
213
- }
214
-
215
- toggleDarkMode() {
216
- darkMode = !darkMode;
217
- updateStatusBar();
218
- notifyListeners();
219
- save();
220
- }
221
-
222
- toggleEngBookNames() {
223
- engTitles = !engTitles;
224
- notifyListeners();
225
- save();
226
- }
227
-
228
- updateStatusBar() {
229
- if (darkMode) {
230
- SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
231
- systemNavigationBarColor: Color(0xFF1F1F22),
232
- statusBarColor: Color(0xFF1F1F22),
233
- systemNavigationBarIconBrightness: Brightness.light,
234
- statusBarIconBrightness: Brightness.light,
235
- ));
236
- } else {
237
- SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
238
- systemNavigationBarColor: Colors.white,
239
- statusBarColor: Colors.white,
240
- systemNavigationBarIconBrightness: Brightness.dark,
241
- statusBarIconBrightness: Brightness.dark,
242
- ));
243
- }
244
- }
245
-
246
- toggleBold() async {
247
- fontBold = !fontBold;
248
- notifyListeners();
249
- save();
250
- }
251
-
252
- increaseFont() async {
253
- textScaleFactor += 0.1;
254
- notifyListeners();
255
- save();
256
- }
257
-
258
- decreaseFont() async {
259
- textScaleFactor -= 0.1;
260
- notifyListeners();
261
- save();
262
- }
263
-
264
- showSettings(BuildContext context) {
265
- // if (isWide(context)) {
266
- // Navigator.of(context).push(
267
- // createNoTransitionPageRoute(
268
- // const ScaffoldMenu(
269
- // backgroundColor: Color(0xFFF2F2F7),
270
- // child: SettingsSheet(),
271
- // ),
272
- // ),
273
- // );
274
- // } else {
275
- showModalBottomSheet(
276
- context: context,
277
- isDismissible: true,
278
- enableDrag: true,
279
- showDragHandle: true,
280
- useSafeArea: true,
281
- builder: (context) => const SettingsSheet(),
282
- );
283
- // }
284
- }
285
-
286
- showActions(BuildContext context) {
287
- actionsShown = true;
288
- Scaffold.of(context).showBottomSheet(
289
- enableDrag: false,
290
- clipBehavior: Clip.antiAliasWithSaveLayer,
291
- (context) => const ActionsSheet(),
292
- );
293
- notifyListeners();
294
- }
295
-
296
- hideActions(BuildContext context) {
297
- if (actionsShown) {
298
- actionsShown = false;
299
- Navigator.of(context).pop();
300
- notifyListeners();
301
- }
302
- }
303
-
304
- showHighlights(BuildContext context) {
305
- highlightsShown = true;
306
- Scaffold.of(context).showBottomSheet(
307
- enableDrag: false,
308
- clipBehavior: Clip.antiAliasWithSaveLayer,
309
- (context) => const HighlightSheet(),
310
- );
311
- notifyListeners();
312
- }
313
-
314
- hideHighlights(BuildContext context) {
315
- if (highlightsShown) {
316
- highlightsShown = false;
317
- Navigator.of(context).pop();
318
- notifyListeners();
319
- }
320
- }
321
-
322
- bool hasNote(Verse v) {
323
- return box.hasData("${v.book}:${v.chapter}:${v.index}:note");
324
- }
325
-
326
- showNoteField(BuildContext context, Verse v) {
327
- final noteText = box.read("${v.book}:${v.chapter}:${v.index}:note") ?? "";
328
- noteTextController.text = noteText;
329
- showModalBottomSheet(
330
- context: context,
331
- isDismissible: true,
332
- enableDrag: true,
333
- showDragHandle: true,
334
- useSafeArea: true,
335
- isScrollControlled: true,
336
- builder: (context) => NoteSheet(verse: v),
337
- );
338
- }
339
-
340
- saveNote(BuildContext context, Verse v) {
341
- final note = noteTextController.text;
342
- box.write("${v.book}:${v.chapter}:${v.index}:note", note);
343
- box.save();
344
- hideNoteField(context);
345
- clearSelections();
346
- hideActions(context);
347
- notifyListeners();
348
- }
349
-
350
- deleteNote(BuildContext context, Verse v) {
351
- box.remove("${v.book}:${v.chapter}:${v.index}:note");
352
- box.save();
353
- notifyListeners();
354
- hideNoteField(context);
355
- }
356
-
357
- hideNoteField(BuildContext context) {
358
- Navigator.of(context).pop();
359
- }
360
-
361
- Color? getHighlight(Verse v) {
362
- final key = "${v.book}:${v.chapter}:${v.index}:highlight";
363
- if (box.hasData(key)) {
364
- // box.remove(key);
365
- // print(box.read(key));
366
- final index = box.read<int>(key);
367
- if (index == null) {
368
- return null;
369
- }
370
- return darkMode ? darkHighlights[index] : lightHighlights[index];
371
- }
372
- return null;
373
- }
374
-
375
- TextStyle getHighlightStyle(BuildContext context, Verse v) {
376
- if (isVerseSelected(v)) {
377
- return TextStyle(
378
- backgroundColor: darkMode ? Colors.grey.shade800 : Colors.grey.shade200,
379
- );
380
- }
381
- if (darkMode) {
382
- // return TextStyle(
383
- // color: getHighlight(v) ?? context.theme.colorScheme.onBackground,
384
- // );
385
- return TextStyle(
386
- backgroundColor: getHighlight(v)?.withOpacity(0.7),
387
- color: getHighlight(v) != null ? Colors.white : context.theme.colorScheme.onBackground,
388
- );
389
- }
390
- return TextStyle(
391
- backgroundColor: getHighlight(v) ?? context.theme.colorScheme.background,
392
- );
393
- }
394
-
395
- void setHighlight(BuildContext context, List<Verse> verses, int index) {
396
- for (final v in verses) {
397
- box.write("${v.book}:${v.chapter}:${v.index}:highlight", index);
398
- }
399
- box.save();
400
- }
401
-
402
- void removeHighlight(BuildContext context, List<Verse> verses) {
403
- for (final v in verses) {
404
- box.remove("${v.book}:${v.chapter}:${v.index}:highlight");
405
- }
406
- box.save();
407
- }
408
-
409
- void shareAppLink(BuildContext context) {
410
- if (isAndroid()) {
411
- Share.share(
412
- subject: "Only Bible App",
413
- "https://play.google.com/store/apps/details?id=${packageInfo.packageName}",
414
- );
415
- } else if (isIOS()) {
416
- Share.share(
417
- subject: "Only Bible App",
418
- "https://apps.apple.com/us/app/hare-pro/id123",
419
- );
420
- } else {
421
- Share.share(
422
- subject: "Only Bible App",
423
- "https://onlybible.app",
424
- );
425
- }
426
- }
427
-
428
- void rateApp(BuildContext context) {
429
- if (isAndroid()) {
430
- openUrl(context, "https://play.google.com/store/apps/details?id=${packageInfo.packageName}");
431
- } else if (isIOS()) {
432
- openUrl(context, "https://apps.apple.com/us/app/only-bible-app/${packageInfo.packageName}");
433
- }
434
- }
435
-
436
- showPrivacyPolicy(BuildContext context) {
437
- Navigator.of(context).push(
438
- createNoTransitionPageRoute(
439
- const ScaffoldMarkdown(title: "Privacy Policy", file: "privacy-policy.md"),
440
- ),
441
- );
442
- }
443
-
444
- showAboutUs(BuildContext context) {
445
- Navigator.of(context).push(
446
- createNoTransitionPageRoute(
447
- const ScaffoldMarkdown(title: "About Us", file: "about-us.md"),
448
- ),
449
- );
450
- }
451
-
452
- bool hasSelectedVerses() {
453
- return selectedVerses.isNotEmpty;
454
- }
455
-
456
- void clearSelections() {
457
- selectedVerses.clear();
458
- notifyListeners();
459
- }
460
-
461
- void removeSelectedHighlights(BuildContext context) {
462
- AppProvider.ofEvent(context).removeHighlight(context, selectedVerses);
463
- selectedVerses.clear();
464
- AppProvider.ofEvent(context).hideActions(context);
465
- notifyListeners();
466
- }
467
-
468
- void closeActions(BuildContext context) {
469
- selectedVerses.clear();
470
- AppProvider.ofEvent(context).hideActions(context);
471
- notifyListeners();
472
- }
473
-
474
- bool isVerseSelected(Verse v) {
475
- return selectedVerses.any((el) => el.book == v.book && el.chapter == v.chapter && el.index == v.index);
476
- }
477
-
478
- void onVerseSelected(BuildContext context, Verse v) {
479
- if (selectedVerses.isEmpty) {
480
- AppProvider.ofEvent(context).showActions(context);
481
- }
482
- if (isVerseSelected(v)) {
483
- selectedVerses.removeWhere((it) => it.index == v.index);
484
- } else {
485
- selectedVerses.add(v);
486
- }
487
- if (selectedVerses.isEmpty) {
488
- AppProvider.ofEvent(context).hideActions(context);
489
- }
490
- notifyListeners();
491
- }
492
-
493
- void shareVerses(BuildContext context) {
494
- final name = bible.books[selectedVerses.first.book].name(context);
495
- final chapter = selectedVerses.first.chapter + 1;
496
- final title = "$name $chapter: ${selectedVerses.map((e) => e.index + 1).join(", ")}";
497
- final text = selectedVerses.map((e) => e.text).join("\n");
498
- Share.share("$title\n$text", subject: title);
499
- }
500
-
501
- pause() async {
502
- await player.pause();
503
- isPlaying = false;
504
- notifyListeners();
505
- }
506
-
507
- onPlay(BuildContext context) async {
508
- final versesToPlay = List.from(selectedVerses);
509
- if (isPlaying) {
510
- pause();
511
- } else {
512
- isPlaying = true;
513
- notifyListeners();
514
- for (final v in versesToPlay) {
515
- final bibleName = bible.name;
516
- final book = (v.book + 1).toString().padLeft(2, "0");
517
- final chapter = (v.chapter + 1).toString().padLeft(3, "0");
518
- final verseNo = (v.index + 1).toString().padLeft(3, "0");
519
- final pathname = "$bibleName/$book-$chapter-$verseNo.mp3";
520
- try {
521
- final url = await FirebaseStorage.instance.ref(pathname).getDownloadURL();
522
- await player.setUrl(url);
523
- await player.play();
524
- await player.stop();
525
- } catch (err) {
526
- log("Could not play audio", name: "play", error: (err.toString(), pathname));
527
- FirebaseCrashlytics.instance.recordFlutterError(FlutterErrorDetails(exception: (err.toString(), pathname)));
528
- if (context.mounted) {
529
- showError(context, context.lEvent.audioError);
530
- }
531
- return;
532
- } finally {
533
- pause();
534
- }
535
- }
536
- }
537
- }
538
- }
lib/screens/bible_select_screen.dart CHANGED
@@ -1,5 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
- import "package:only_bible_app/providers/app_provider.dart";
2
+ import "package:only_bible_app/navigation.dart";
3
+ import "package:only_bible_app/state.dart";
3
4
  import "package:only_bible_app/utils.dart";
4
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
5
6
  import "package:only_bible_app/widgets/sliver_heading.dart";
@@ -14,7 +15,7 @@ class BibleSelectScreen extends StatelessWidget {
14
15
  child: CustomScrollView(
15
16
  physics: const BouncingScrollPhysics(),
16
17
  slivers: [
17
- SliverHeading(title: context.l.bibleSelectTitle, showClose: !context.app.firstOpen),
18
+ SliverHeading(title: context.l.bibleSelectTitle, showClose: !firstOpen.value),
18
19
  SliverTileGrid(
19
20
  listType: ListType.large,
20
21
  children: List.of(
@@ -28,10 +29,10 @@ class BibleSelectScreen extends StatelessWidget {
28
29
  // ],
29
30
  // ),
30
31
  onPressed: () {
31
- AppProvider.ofEvent(context).updateCurrentBible(context, Locale(l.localeName), l.languageTitle);
32
+ updateCurrentBible(context, Locale(l.localeName), l.languageTitle);
32
- if (context.appEvent.firstOpen) {
33
+ if (firstOpen.value) {
33
- context.appEvent.firstOpen = false;
34
+ firstOpen.set!();
34
- context.appEvent.pushBookChapter(context, 0, 0, null);
35
+ pushBookChapter(context, 0, 0, null);
35
36
  } else {
36
37
  Navigator.of(context).pop();
37
38
  }
lib/screens/chapter_select_screen.dart CHANGED
@@ -1,5 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/models.dart";
3
+ import "package:only_bible_app/navigation.dart";
3
4
  import "package:only_bible_app/utils.dart";
4
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
5
6
  import "package:only_bible_app/widgets/sliver_tile_grid.dart";
@@ -22,7 +23,7 @@ class ChapterSelectScreen extends StatelessWidget {
22
23
  children: List.generate(book.chapters.length, (index) {
23
24
  return TextButton(
24
25
  child: Text("${index + 1}"),
25
- onPressed: () => context.appEvent.replaceBookChapter(context, selectedBookIndex, index),
26
+ onPressed: () => replaceBookChapter(context, selectedBookIndex, index),
26
27
  );
27
28
  }),
28
29
  ),
lib/screens/chapter_view_screen.dart CHANGED
@@ -1,8 +1,10 @@
1
1
  import "package:flutter/material.dart";
2
+ import "package:only_bible_app/state.dart";
2
3
  import "package:only_bible_app/utils.dart";
3
4
  import "package:only_bible_app/widgets/chapter_app_bar.dart";
4
5
  import "package:only_bible_app/widgets/sidebar.dart";
5
6
  import "package:only_bible_app/widgets/verses_view.dart";
7
+ import "package:provider/provider.dart";
6
8
 
7
9
  class ChapterViewScreen extends StatelessWidget {
8
10
  final int bookIndex;
@@ -29,7 +31,7 @@ class ChapterViewScreen extends StatelessWidget {
29
31
  // );
30
32
  // },
31
33
  // ),
32
- final book = context.app.bible.books[bookIndex];
34
+ final book = bible.watch(context).books[bookIndex];
33
35
  final chapter = book.chapters[chapterIndex];
34
36
  return Scaffold(
35
37
  appBar: context.isWide ? null : ChapterAppBar(book: book, chapter: chapter),
@@ -48,14 +50,14 @@ class ChapterViewScreen extends StatelessWidget {
48
50
  child: Divider(height: 5, indent: 20, endIndent: 20, thickness: 1.5),
49
51
  ),
50
52
  Flexible(
51
- child: VersesView(book: book, chapter: chapter),
53
+ child: VersesView(chapter: chapter),
52
54
  ),
53
55
  ],
54
56
  ),
55
57
  ),
56
58
  ],
57
59
  )
58
- : VersesView(book: book, chapter: chapter),
60
+ : VersesView(chapter: chapter),
59
61
  ),
60
62
  );
61
63
  }
lib/sheets/actions_sheet.dart CHANGED
@@ -1,6 +1,7 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/dialog.dart";
3
- import "package:only_bible_app/providers/app_provider.dart";
3
+ import "package:only_bible_app/navigation.dart";
4
+ import "package:only_bible_app/state.dart";
4
5
  import "package:only_bible_app/utils.dart";
5
6
 
6
7
  class ActionsSheet extends StatelessWidget {
@@ -8,11 +9,10 @@ class ActionsSheet extends StatelessWidget {
8
9
 
9
10
  @override
10
11
  Widget build(BuildContext context) {
11
- final app = AppProvider.of(context);
12
12
  final bottom = isIOS() ? 20.0 : 0.0;
13
- final iconColor = app.darkMode ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
13
+ final iconColor = darkMode.value ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
14
- final audioIcon = app.isPlaying ? Icons.pause_circle_outline : Icons.play_circle_outline;
14
+ final audioIcon = isPlaying.watch(context) ? Icons.pause_circle_outline : Icons.play_circle_outline;
15
- final audioEnabled = app.hasAudio(context);
15
+ final audioEnabled = context.hasAudio;
16
16
  return Container(
17
17
  height: context.actionsHeight,
18
18
  color: Theme.of(context).colorScheme.background,
@@ -22,19 +22,19 @@ class ActionsSheet extends StatelessWidget {
22
22
  children: [
23
23
  IconButton(
24
24
  padding: EdgeInsets.zero,
25
- onPressed: () => context.appEvent.removeSelectedHighlights(context),
25
+ onPressed: () => removeHighlight(context),
26
26
  icon: Icon(Icons.cancel_outlined, size: 28, color: iconColor),
27
27
  ),
28
28
  IconButton(
29
29
  padding: EdgeInsets.zero,
30
- onPressed: () => context.appEvent.showHighlights(context),
30
+ onPressed: () => showHighlights(context),
31
31
  icon: Icon(Icons.border_color_outlined, size: 28, color: iconColor),
32
32
  ),
33
33
  IconButton(
34
34
  padding: EdgeInsets.zero,
35
35
  onPressed: () {
36
36
  if (audioEnabled) {
37
- context.appEvent.onPlay(context);
37
+ onPlay(context);
38
38
  } else {
39
39
  showError(context, context.lEvent.audioNotAvailable);
40
40
  }
@@ -43,12 +43,12 @@ class ActionsSheet extends StatelessWidget {
43
43
  ),
44
44
  IconButton(
45
45
  padding: EdgeInsets.zero,
46
- onPressed: () => context.appEvent.showNoteField(context, context.appEvent.selectedVerses.first),
46
+ onPressed: () => showNoteField(context, selectedVerses.value.first),
47
47
  icon: Icon(Icons.post_add_outlined, size: 34, color: iconColor),
48
48
  ),
49
49
  IconButton(
50
50
  padding: EdgeInsets.zero,
51
- onPressed: () => context.appEvent.shareVerses(context),
51
+ onPressed: () => shareVerses(context, bible.value, selectedVerses.value),
52
52
  icon: Icon(Icons.share_outlined, size: 34, color: iconColor),
53
53
  ),
54
54
  ],
lib/sheets/highlight_sheet.dart CHANGED
@@ -1,4 +1,5 @@
1
1
  import "package:flutter/material.dart";
2
+ import "package:only_bible_app/state.dart";
2
3
  import "package:only_bible_app/theme.dart";
3
4
  import "package:only_bible_app/utils.dart";
4
5
  import "package:only_bible_app/widgets/highlight_button.dart";
@@ -9,11 +10,9 @@ class HighlightSheet extends StatelessWidget {
9
10
  @override
10
11
  Widget build(BuildContext context) {
11
12
  final bottom = isIOS() ? 20.0 : 0.0;
12
- final iconColor = context.app.darkMode ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
13
+ final iconColor = darkMode.value ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
13
14
  void onHighlight(int index) {
14
- final verses = context.appEvent.selectedVerses;
15
- context.appEvent.setHighlight(context, verses, index);
15
+ setHighlight(context, index);
16
- context.appEvent.closeActions(context);
17
16
  }
18
17
 
19
18
  return Container(
@@ -25,27 +24,27 @@ class HighlightSheet extends StatelessWidget {
25
24
  children: [
26
25
  IconButton(
27
26
  padding: EdgeInsets.zero,
28
- onPressed: () => context.appEvent.removeSelectedHighlights(context),
27
+ onPressed: () => removeHighlight(context),
29
28
  icon: Icon(Icons.cancel_outlined, size: 28, color: iconColor),
30
29
  ),
31
30
  HighlightButton(
32
31
  index: 0,
33
- color: context.app.darkMode ? darkHighlights[0] : lightHighlights[0],
32
+ color: darkMode.value ? darkHighlights[0] : lightHighlights[0],
34
33
  onHighlightSelected: onHighlight,
35
34
  ),
36
35
  HighlightButton(
37
36
  index: 1,
38
- color: context.app.darkMode ? darkHighlights[1] : lightHighlights[1],
37
+ color: darkMode.value ? darkHighlights[1] : lightHighlights[1],
39
38
  onHighlightSelected: onHighlight,
40
39
  ),
41
40
  HighlightButton(
42
41
  index: 2,
43
- color: context.app.darkMode ? darkHighlights[2] : lightHighlights[2],
42
+ color: darkMode.value ? darkHighlights[2] : lightHighlights[2],
44
43
  onHighlightSelected: onHighlight,
45
44
  ),
46
45
  HighlightButton(
47
46
  index: 3,
48
- color: context.app.darkMode ? darkHighlights[3] : lightHighlights[3],
47
+ color: darkMode.value ? darkHighlights[3] : lightHighlights[3],
49
48
  onHighlightSelected: onHighlight,
50
49
  ),
51
50
  ],
lib/sheets/settings_sheet.dart CHANGED
@@ -1,4 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
+ import "package:only_bible_app/navigation.dart";
3
+ import "package:only_bible_app/state.dart";
2
4
  import "package:only_bible_app/utils.dart";
3
5
  import "package:settings_ui/settings_ui.dart";
4
6
 
@@ -24,20 +26,20 @@ class SettingsSheet extends StatelessWidget {
24
26
  SettingsTile.navigation(
25
27
  leading: const Icon(Icons.book_outlined, color: Colors.blueAccent),
26
28
  title: Text(context.l.bibleTitle),
27
- value: Text(context.app.bible.name),
29
+ value: Text(bible.watch(context).name),
28
- onPressed: context.app.changeBible,
30
+ onPressed: changeBible,
29
31
  ),
30
32
  SettingsTile.navigation(
31
- leading: const Icon(Icons.color_lens_outlined, color: Colors.pink),
33
+ leading: const Icon(Icons.color_lens_outlined, color: Colors.green),
32
34
  title: Text(context.l.themeTitle),
33
35
  trailing: ToggleButtons(
34
36
  onPressed: (int index) {
35
- context.appEvent.toggleDarkMode();
37
+ darkMode.set!();
36
38
  },
37
39
  highlightColor: Colors.transparent,
38
40
  borderColor: Colors.grey,
39
41
  borderRadius: const BorderRadius.all(Radius.circular(25)),
40
- selectedColor: context.app.darkMode ? Colors.lightBlue.shade300 : Colors.yellowAccent.shade700,
42
+ selectedColor: darkMode.value ? Colors.lightBlue.shade300 : Colors.yellowAccent.shade700,
41
43
  selectedBorderColor: Colors.grey,
42
44
  color: Colors.grey,
43
45
  fillColor: Colors.transparent,
@@ -45,7 +47,7 @@ class SettingsSheet extends StatelessWidget {
45
47
  minHeight: 36.0,
46
48
  minWidth: 50.0,
47
49
  ),
48
- isSelected: [!context.app.darkMode, context.app.darkMode],
50
+ isSelected: [!darkMode.value, darkMode.value],
49
51
  children: const [
50
52
  Icon(Icons.light_mode),
51
53
  Icon(Icons.dark_mode),
@@ -56,7 +58,7 @@ class SettingsSheet extends StatelessWidget {
56
58
  title: Text(context.l.incrementFontTitle),
57
59
  leading: Icon(Icons.font_download, color: context.theme.colorScheme.onBackground),
58
60
  trailing: IconButton(
59
- onPressed: context.appEvent.increaseFont,
61
+ onPressed: () => textScale.update!(0.1),
60
62
  icon: const Icon(Icons.add_circle_outline, size: 32, color: Colors.redAccent),
61
63
  ),
62
64
  ),
@@ -64,21 +66,21 @@ class SettingsSheet extends StatelessWidget {
64
66
  title: Text(context.l.decrementFontTitle),
65
67
  leading: Icon(Icons.font_download, color: context.theme.colorScheme.onBackground),
66
68
  trailing: IconButton(
67
- onPressed: context.appEvent.decreaseFont,
69
+ onPressed: () => textScale.update!(-0.1),
68
70
  icon: const Icon(Icons.remove_circle_outline, size: 32, color: Colors.blueAccent),
69
71
  ),
70
72
  ),
71
73
  SettingsTile.switchTile(
72
- initialValue: context.app.fontBold,
74
+ initialValue: fontBold.watch(context),
73
75
  leading: Icon(Icons.format_bold, color: context.theme.colorScheme.onBackground),
74
76
  title: Text(context.l.boldFontTitle),
75
- onToggle: (value) => context.appEvent.toggleBold(),
77
+ onToggle: (value) => fontBold.set!(),
76
78
  ),
77
79
  SettingsTile.switchTile(
78
- initialValue: context.app.engTitles,
80
+ initialValue: engTitles.watch(context),
79
81
  leading: Icon(Icons.abc, color: context.theme.colorScheme.onBackground),
80
82
  title: Text(context.l.engTitles),
81
- onToggle: (value) => context.appEvent.toggleEngBookNames(),
83
+ onToggle: (value) => engTitles.set!(),
82
84
  ),
83
85
  ],
84
86
  ),
@@ -89,23 +91,23 @@ class SettingsSheet extends StatelessWidget {
89
91
  SettingsTile.navigation(
90
92
  leading: const Icon(Icons.policy_outlined, color: Colors.brown),
91
93
  title: Text(context.l.privacyPolicyTitle),
92
- onPressed: context.appEvent.showPrivacyPolicy,
94
+ onPressed: showPrivacyPolicy,
93
95
  ),
94
96
  SettingsTile.navigation(
95
97
  leading: const Icon(Icons.share_outlined, color: Colors.blueAccent),
96
98
  title: Text(context.l.shareAppTitle),
97
- onPressed: context.appEvent.shareAppLink,
99
+ onPressed: shareAppLink,
98
100
  ),
99
101
  if (!isDesktop()) // TODO: mabe support OSx if we release in that store
100
102
  SettingsTile.navigation(
101
103
  leading: Icon(Icons.star, color: Colors.yellowAccent.shade700),
102
104
  title: Text(context.l.rateAppTitle),
103
- onPressed: context.appEvent.rateApp,
105
+ onPressed: rateApp,
104
106
  ),
105
107
  SettingsTile.navigation(
106
108
  leading: Icon(Icons.info_outline, color: context.theme.colorScheme.onBackground),
107
109
  title: Text(context.l.aboutUsTitle),
108
- onPressed: context.appEvent.showAboutUs,
110
+ onPressed: showAboutUs,
109
111
  ),
110
112
  ],
111
113
  ),
lib/state.dart ADDED
@@ -0,0 +1,300 @@
1
+ import "dart:developer";
2
+ import "package:firebase_crashlytics/firebase_crashlytics.dart";
3
+ import "package:firebase_storage/firebase_storage.dart";
4
+ import "package:flutter/material.dart";
5
+ import "package:get_storage/get_storage.dart";
6
+ import "package:just_audio/just_audio.dart";
7
+ import "package:only_bible_app/atom.dart";
8
+ import "package:only_bible_app/dialog.dart";
9
+ import "package:only_bible_app/models.dart";
10
+ import "package:only_bible_app/theme.dart";
11
+ import "package:only_bible_app/utils.dart";
12
+ import "package:only_bible_app/navigation.dart";
13
+ import "package:only_bible_app/widgets/note_sheet.dart";
14
+
15
+ final box = GetStorage("only-bible-app-prefs");
16
+ final player = AudioPlayer();
17
+ final noteTextController = TextEditingController();
18
+
19
+ initState() async {
20
+ await ensureAtomsInitialized(box);
21
+ }
22
+
23
+ final Atom<bool> firstOpen = Atom<bool>(
24
+ key: "firstOpen",
25
+ initialValue: true,
26
+ set: () {
27
+ firstOpen.value = false;
28
+ },
29
+ );
30
+
31
+ final Atom<String> languageCode = Atom<String>(
32
+ key: "languageCode",
33
+ initialValue: "en",
34
+ update: (String v) {
35
+ languageCode.value = v;
36
+ },
37
+ );
38
+
39
+ final Atom<String> bibleName = Atom<String>(
40
+ key: "bibleName",
41
+ initialValue: "English",
42
+ update: (String v) {
43
+ bibleName.value = v;
44
+ },
45
+ );
46
+
47
+ final Atom<Bible> bible = Atom<Bible>(
48
+ key: "bible",
49
+ persist: false,
50
+ initialValue: Bible(name: "English"),
51
+ update: (Bible v) {
52
+ bible.value = v;
53
+ },
54
+ );
55
+
56
+ updateCurrentBible(BuildContext context, Locale l, String name) async {
57
+ languageCode.value = l.languageCode;
58
+ bibleName.value = name;
59
+ await loadBible();
60
+ }
61
+
62
+ loadBible() async {
63
+ // Trace customTrace;
64
+ // if (!isDesktop()) {
65
+ // customTrace = FirebasePerformance.instance.newTrace("loadBible");
66
+ // await customTrace.start();
67
+ // }
68
+ final books = await getBibleFromAsset(bibleName.value);
69
+ // if (!isDesktop()) {
70
+ // await customTrace.stop();
71
+ // }
72
+ bible.update!(Bible.withBooks(
73
+ name: bibleName.value,
74
+ books: books,
75
+ ));
76
+ }
77
+
78
+ final Atom<bool> engTitles = Atom<bool>(
79
+ key: "engTitles",
80
+ initialValue: false,
81
+ set: () {
82
+ engTitles.value = !engTitles.value;
83
+ },
84
+ );
85
+
86
+ final Atom<bool> darkMode = Atom<bool>(
87
+ key: "darkMode",
88
+ initialValue: false,
89
+ set: () {
90
+ darkMode.value = !darkMode.value;
91
+ updateStatusBar(darkMode.value);
92
+ },
93
+ );
94
+
95
+ final Atom<bool> fontBold = Atom<bool>(
96
+ key: "fontBold",
97
+ initialValue: false,
98
+ set: () {
99
+ fontBold.value = !fontBold.value;
100
+ },
101
+ );
102
+
103
+ final Atom<double> textScale = Atom<double>(
104
+ key: "textScale",
105
+ initialValue: 0,
106
+ update: (double v) {
107
+ textScale.value += v;
108
+ },
109
+ );
110
+
111
+ final Atom<int> savedBook = Atom<int>(
112
+ key: "savedBook",
113
+ initialValue: 0,
114
+ update: (int v) {
115
+ savedBook.value = v;
116
+ },
117
+ );
118
+
119
+ final Atom<int> savedChapter = Atom<int>(
120
+ key: "savedChapter",
121
+ initialValue: 0,
122
+ update: (int v) {
123
+ savedChapter.value = v;
124
+ },
125
+ );
126
+
127
+ final Atom<bool> isPlaying = Atom<bool>(
128
+ key: "isPlaying",
129
+ initialValue: false,
130
+ persist: false,
131
+ update: (bool v) {
132
+ isPlaying.value = v;
133
+ },
134
+ );
135
+
136
+ final Atom<List<Verse>> selectedVerses = Atom<List<Verse>>(
137
+ key: "selectedVerses",
138
+ initialValue: [],
139
+ persist: false,
140
+ update: (List<Verse> verses) {
141
+ selectedVerses.value = verses;
142
+ // selectedVerses.notifyChanged();
143
+ },
144
+ );
145
+
146
+ void clearSelections() {
147
+ selectedVerses.value.clear();
148
+ }
149
+
150
+ Color? getHighlight(Verse v) {
151
+ final key = "${v.book}:${v.chapter}:${v.index}:highlight";
152
+ if (box.hasData(key)) {
153
+ // box.remove(key);
154
+ // print(box.read(key));
155
+ final index = box.read<int>(key);
156
+ if (index == null) {
157
+ return null;
158
+ }
159
+ return darkMode.value ? darkHighlights[index] : lightHighlights[index];
160
+ }
161
+ return null;
162
+ }
163
+
164
+ void setHighlight(BuildContext context, int index) {
165
+ for (final v in selectedVerses.value) {
166
+ box.write("${v.book}:${v.chapter}:${v.index}:highlight", index);
167
+ }
168
+ box.save();
169
+ hideActions(context);
170
+ }
171
+
172
+ void removeHighlight(BuildContext context) {
173
+ for (final v in selectedVerses.value) {
174
+ box.remove("${v.book}:${v.chapter}:${v.index}:highlight");
175
+ }
176
+ box.save();
177
+ hideActions(context);
178
+ }
179
+
180
+ bool isVerseSelected(Verse v) {
181
+ return selectedVerses.value.any((el) => el.book == v.book && el.chapter == v.chapter && el.index == v.index);
182
+ }
183
+
184
+ bool watchVerseSelected(BuildContext context, Verse v) {
185
+ return selectedVerses.watch(context).any((el) => el.book == v.book && el.chapter == v.chapter && el.index == v.index);
186
+ }
187
+
188
+ void onVerseSelected(BuildContext context, Verse v) {
189
+ if (isVerseSelected(v)) {
190
+ selectedVerses.update!(selectedVerses.value.removeBy((it) => it.index == v.index).toList());
191
+ } else {
192
+ selectedVerses.update!(selectedVerses.value.addBy(v).toList());
193
+ }
194
+ if (selectedVerses.value.isNotEmpty) {
195
+ showActions(context);
196
+ }
197
+ if (selectedVerses.value.isEmpty) {
198
+ hideActions(context);
199
+ }
200
+ }
201
+
202
+ TextStyle getHighlightStyle(BuildContext context, Verse v) {
203
+ if (watchVerseSelected(context, v)) {
204
+ return TextStyle(
205
+ backgroundColor: darkMode.value ? Colors.grey.shade800 : Colors.grey.shade200,
206
+ );
207
+ }
208
+ if (darkMode.watch(context)) {
209
+ // return TextStyle(
210
+ // color: getHighlight(v) ?? context.theme.colorScheme.onBackground,
211
+ // );
212
+ return TextStyle(
213
+ backgroundColor: getHighlight(v)?.withOpacity(0.7),
214
+ color: getHighlight(v) != null ? Colors.white : context.theme.colorScheme.onBackground,
215
+ );
216
+ }
217
+ return TextStyle(
218
+ backgroundColor: getHighlight(v) ?? context.theme.colorScheme.background,
219
+ );
220
+ }
221
+
222
+ clearEvents(BuildContext context) {
223
+ if (isPlaying.value) {
224
+ pause();
225
+ }
226
+ hideActions(context);
227
+ }
228
+
229
+ pause() async {
230
+ await player.pause();
231
+ isPlaying.value = false;
232
+ }
233
+
234
+ onPlay(BuildContext context) async {
235
+ final versesToPlay = List.from(selectedVerses.value);
236
+ if (isPlaying.value) {
237
+ pause();
238
+ } else {
239
+ isPlaying.value = true;
240
+ for (final v in versesToPlay) {
241
+ final book = (v.book + 1).toString().padLeft(2, "0");
242
+ final chapter = (v.chapter + 1).toString().padLeft(3, "0");
243
+ final verseNo = (v.index + 1).toString().padLeft(3, "0");
244
+ final pathname = "${bibleName.value}/$book-$chapter-$verseNo.mp3";
245
+ try {
246
+ final url = await FirebaseStorage.instance.ref(pathname).getDownloadURL();
247
+ await player.setUrl(url);
248
+ await player.play();
249
+ await player.stop();
250
+ } catch (err) {
251
+ log("Could not play audio", name: "play", error: (err.toString(), pathname));
252
+ FirebaseCrashlytics.instance.recordFlutterError(FlutterErrorDetails(exception: (err.toString(), pathname)));
253
+ if (context.mounted) {
254
+ showError(context, context.lEvent.audioError);
255
+ }
256
+ return;
257
+ } finally {
258
+ pause();
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ bool hasNote(Verse v) {
265
+ return box.hasData("${v.book}:${v.chapter}:${v.index}:note");
266
+ }
267
+
268
+ showNoteField(BuildContext context, Verse v) {
269
+ final noteText = box.read("${v.book}:${v.chapter}:${v.index}:note") ?? "";
270
+ noteTextController.text = noteText;
271
+ showModalBottomSheet(
272
+ context: context,
273
+ isDismissible: true,
274
+ enableDrag: true,
275
+ showDragHandle: true,
276
+ useSafeArea: true,
277
+ isScrollControlled: true,
278
+ builder: (context) => NoteSheet(verse: v),
279
+ );
280
+ }
281
+
282
+ saveNote(BuildContext context, Verse v) {
283
+ final note = noteTextController.text;
284
+ box.write("${v.book}:${v.chapter}:${v.index}:note", note);
285
+ box.save();
286
+ hideNoteField(context);
287
+ hideActions(context);
288
+ }
289
+
290
+ deleteNote(BuildContext context, Verse v) {
291
+ box.remove("${v.book}:${v.chapter}:${v.index}:note");
292
+ box.save();
293
+ hideNoteField(context);
294
+ // TODO: hack to re-render this page
295
+ selectedVerses.notifyChanged();
296
+ }
297
+
298
+ hideNoteField(BuildContext context) {
299
+ Navigator.of(context).pop();
300
+ }
lib/utils.dart CHANGED
@@ -1,6 +1,6 @@
1
1
  import "dart:convert";
2
2
  import "package:only_bible_app/dialog.dart";
3
- import "package:only_bible_app/providers/app_provider.dart";
3
+ import "package:only_bible_app/state.dart";
4
4
  import "package:url_launcher/url_launcher.dart";
5
5
  import "package:flutter/foundation.dart" show defaultTargetPlatform, TargetPlatform;
6
6
  import "package:flutter/material.dart";
@@ -11,22 +11,24 @@ import "package:provider/provider.dart";
11
11
 
12
12
  extension MyIterable<E> on Iterable<E> {
13
13
  Iterable<E> sortedBy(Comparable Function(E e) key) => toList()..sort((a, b) => key(a).compareTo(key(b)));
14
+
15
+ Iterable<E> removeBy(bool Function(E e) key) => toList()..removeWhere(key);
16
+
17
+ Iterable<E> addBy(E e) => toList()..add(e);
14
18
  }
15
19
 
16
20
  extension AppContext on BuildContext {
17
21
  ThemeData get theme => Theme.of(this);
18
22
 
19
- AppLocalizations get l => app.engTitles && app.locale.languageCode != "en"
23
+ AppLocalizations get l => engTitles.value && languageCode.value != "en"
20
24
  ? lookupAppLocalizations(const Locale("en"))
21
25
  : AppLocalizations.of(this)!;
22
26
 
23
- AppLocalizations get lEvent => appEvent.engTitles && appEvent.locale.languageCode != "en"
27
+ AppLocalizations get lEvent => engTitles.value && languageCode.value != "en"
24
28
  ? lookupAppLocalizations(const Locale("en"))
25
29
  : AppLocalizations.of(this)!;
26
30
 
27
- AppProvider get app => Provider.of(this, listen: true);
31
+ get hasAudio => l.hasAudio == "true";
28
-
29
- AppProvider get appEvent => Provider.of(this, listen: false);
30
32
 
31
33
  double get actionsHeight {
32
34
  if (isIOS()) {
lib/widgets/chapter_app_bar.dart CHANGED
@@ -1,5 +1,7 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/models.dart";
3
+ import "package:only_bible_app/navigation.dart";
4
+ import "package:only_bible_app/state.dart";
3
5
  import "package:only_bible_app/utils.dart";
4
6
 
5
7
  class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -24,7 +26,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
24
26
  children: [
25
27
  InkWell(
26
28
  enableFeedback: true,
27
- onTap: () => context.appEvent.changeBook(context),
29
+ onTap: () => changeBook(context, bible.value),
28
30
  child: Row(
29
31
  children: [
30
32
  Text(
@@ -54,7 +56,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
54
56
  ),
55
57
  icon: const Icon(Icons.chevron_left),
56
58
  label: const Text("Prev"),
57
- onPressed: () => context.appEvent.onPrevious(context, book.index, chapter.index),
59
+ onPressed: () => previousChapter(context, bible.value, book.index, chapter.index),
58
60
  ),
59
61
  if (isDesktop) const SizedBox(width: 10),
60
62
  if (isDesktop)
@@ -68,7 +70,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
68
70
  ),
69
71
  icon: const Icon(Icons.chevron_right),
70
72
  label: const Text("Next"),
71
- onPressed: () => context.appEvent.onNext(context, book.index, chapter.index),
73
+ onPressed: () => nextChapter(context, bible.value, book.index, chapter.index),
72
74
  ),
73
75
  if (isDesktop) const SizedBox(width: 20),
74
76
  if (isDesktop)
@@ -86,15 +88,15 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
86
88
  ),
87
89
  ),
88
90
  icon: const Icon(Icons.book_outlined),
89
- label: Text(context.app.bible.name),
91
+ label: Text(bible.watch(context).name),
90
- onPressed: () => context.appEvent.changeBibleFromHeader(context),
92
+ onPressed: () => changeBibleFromHeader(context),
91
93
  ),
92
94
  Padding(
93
95
  padding: const EdgeInsets.only(left: 10),
94
96
  child: IconButton(
95
97
  padding: EdgeInsets.zero,
96
98
  icon: Icon(Icons.more_vert, size: isDesktop ? 28 : 24),
97
- onPressed: () => context.appEvent.showSettings(context),
99
+ onPressed: () => showSettings(context),
98
100
  ),
99
101
  ),
100
102
  ],
lib/widgets/note_sheet.dart CHANGED
@@ -1,7 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/models.dart";
3
- import "package:only_bible_app/providers/app_provider.dart";
4
- import "package:only_bible_app/utils.dart";
3
+ import "package:only_bible_app/state.dart";
5
4
  import "package:only_bible_app/widgets/modal_button.dart";
6
5
 
7
6
  class NoteSheet extends StatelessWidget {
@@ -11,7 +10,6 @@ class NoteSheet extends StatelessWidget {
11
10
 
12
11
  @override
13
12
  Widget build(BuildContext context) {
14
- final app = AppProvider.of(context);
15
13
  return Container(
16
14
  padding: EdgeInsets.only(
17
15
  left: 10,
@@ -25,7 +23,7 @@ class NoteSheet extends StatelessWidget {
25
23
  Padding(
26
24
  padding: const EdgeInsets.only(bottom: 5, left: 15),
27
25
  child: Text(
28
- "Note on ${app.bible.books[verse.book].name(context)} ${verse.chapter + 1}:${verse.index + 1}",
26
+ "Note on ${bible.watch(context).books[verse.book].name(context)} ${verse.chapter + 1}:${verse.index + 1}",
29
27
  style: Theme.of(context).textTheme.headlineMedium,
30
28
  ),
31
29
  ),
@@ -33,7 +31,7 @@ class NoteSheet extends StatelessWidget {
33
31
  margin: const EdgeInsets.all(12),
34
32
  height: 8 * 24.0,
35
33
  child: TextField(
36
- controller: app.noteTextController,
34
+ controller: noteTextController,
37
35
  maxLines: 100,
38
36
  keyboardType: TextInputType.multiline,
39
37
  decoration: const InputDecoration(filled: true, hintText: "Add a note"),
@@ -47,9 +45,9 @@ class NoteSheet extends StatelessWidget {
47
45
  child: Row(
48
46
  mainAxisAlignment: MainAxisAlignment.start,
49
47
  children: [
50
- if (app.noteTextController.value.text != "")
48
+ if (noteTextController.value.text != "")
51
49
  ModalButton(
52
- onPressed: () => app.deleteNote(context, verse),
50
+ onPressed: () => deleteNote(context, verse),
53
51
  icon: Icons.delete_outline,
54
52
  label: "Delete",
55
53
  ),
@@ -59,9 +57,7 @@ class NoteSheet extends StatelessWidget {
59
57
  children: [
60
58
  ModalButton(
61
59
  onPressed: () {
62
- context.appEvent.saveNote(context, verse);
60
+ saveNote(context, verse);
63
- // context.chapterEvent.clearSelections();
64
- // context.appEvent.hideActions(context);
65
61
  },
66
62
  icon: Icons.save_outlined,
67
63
  label: "Save",
lib/widgets/scaffold_menu.dart CHANGED
@@ -1,4 +1,5 @@
1
1
  import "package:flutter/material.dart";
2
+ import "package:only_bible_app/state.dart";
2
3
  import "package:only_bible_app/utils.dart";
3
4
 
4
5
  class ScaffoldMenu extends StatelessWidget {
@@ -10,7 +11,7 @@ class ScaffoldMenu extends StatelessWidget {
10
11
  @override
11
12
  Widget build(BuildContext context) {
12
13
  final pageWidth = MediaQuery.of(context).size.width;
13
- final isWide = context.isWide && !context.app.firstOpen;
14
+ final isWide = context.isWide && !firstOpen.value;
14
15
  return Scaffold(
15
16
  backgroundColor: isWide ? Colors.transparent : context.theme.colorScheme.background,
16
17
  body: SafeArea(
lib/widgets/verses_view.dart CHANGED
@@ -2,24 +2,23 @@ import "package:flutter/gestures.dart";
2
2
  import "package:flutter/material.dart";
3
3
  import "package:flutter_swipe_detector/flutter_swipe_detector.dart";
4
4
  import "package:only_bible_app/models.dart";
5
- import "package:only_bible_app/providers/app_provider.dart";
5
+ import "package:only_bible_app/navigation.dart";
6
- import "package:only_bible_app/utils.dart";
6
+ import "package:only_bible_app/state.dart";
7
7
 
8
8
  class VersesView extends StatelessWidget {
9
- final Book book;
10
9
  final Chapter chapter;
10
+
11
- const VersesView({super.key, required this.book, required this.chapter});
11
+ const VersesView({super.key, required this.chapter});
12
12
 
13
13
  @override
14
14
  Widget build(BuildContext context) {
15
- final app = AppProvider.of(context);
16
15
  final textStyle = DefaultTextStyle.of(context).style;
17
16
  return SwipeDetector(
18
17
  onSwipeLeft: (offset) {
19
- context.appEvent.onNext(context, book.index, chapter.index);
18
+ nextChapter(context, bible.value, chapter.book, chapter.index);
20
19
  },
21
20
  onSwipeRight: (offset) {
22
- context.appEvent.onPrevious(context, book.index, chapter.index);
21
+ previousChapter(context, bible.value, chapter.book, chapter.index);
23
22
  },
24
23
  child: Padding(
25
24
  padding: const EdgeInsets.only(left: 20, right: 20),
@@ -35,19 +34,14 @@ class VersesView extends StatelessWidget {
35
34
  Align(
36
35
  alignment: Alignment.centerLeft,
37
36
  child: Text.rich(
38
- // scrollPhysics: const BouncingScrollPhysics(),
39
- // contextMenuBuilder: null,
40
- textScaleFactor: app.textScaleFactor,
37
+ textScaleFactor: 1.1 + textScale.watch(context),
41
38
  textAlign: TextAlign.left,
42
- // onSelectionChanged: (selection, _) {
43
- // },
44
39
  TextSpan(
45
- style: app.fontBold
40
+ style: fontBold.watch(context)
46
41
  ? textStyle.copyWith(
47
42
  fontWeight: FontWeight.w500,
48
43
  )
49
44
  : textStyle,
50
- // recognizer: TapAndPanGestureRecognizer()..onDragEnd = (e) => print("Hello"),
51
45
  children: chapter.verses
52
46
  .map(
53
47
  (v) => [
@@ -57,13 +51,13 @@ class VersesView extends StatelessWidget {
57
51
  child: Text("${v.index + 1} ", style: Theme.of(context).textTheme.labelMedium),
58
52
  ),
59
53
  ),
60
- if (app.hasNote(v))
54
+ if (hasNote(v))
61
55
  WidgetSpan(
62
56
  child: Padding(
63
57
  padding: const EdgeInsets.only(left: 3, right: 3),
64
58
  child: GestureDetector(
65
59
  onTap: () {
66
- app.showNoteField(context, v);
60
+ showNoteField(context, v);
67
61
  },
68
62
  child: const Icon(
69
63
  Icons.sticky_note_2_outlined,
@@ -75,11 +69,10 @@ class VersesView extends StatelessWidget {
75
69
  ),
76
70
  TextSpan(
77
71
  text: "${v.text}\n",
78
- style: context.app.getHighlightStyle(context, v),
72
+ style: getHighlightStyle(context, v),
79
73
  recognizer: TapGestureRecognizer()
80
74
  ..onTap = () {
81
- context.appEvent.onVerseSelected(context, v);
75
+ onVerseSelected(context, v);
82
- // AppModel.ofEvent(context).showHighlightMenu(context, v, details.globalPosition);
83
76
  },
84
77
  ),
85
78
  const WidgetSpan(
@@ -95,7 +88,7 @@ class VersesView extends StatelessWidget {
95
88
  ),
96
89
  ),
97
90
  Padding(
98
- padding: EdgeInsets.only(bottom: app.actionsShown ? 120 : 0),
91
+ padding: EdgeInsets.only(bottom: actionsShown.watch(context) ? 120 : 0),
99
92
  ),
100
93
  ],
101
94
  ),