~repos /only-bible-app

#kotlin#android#ios

GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone https://git.pyrossh.dev/only-bible-app.git
Discussions: https://groups.google.com/g/rust-embed-devs

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


lib/app.dart CHANGED
@@ -1,4 +1,4 @@
1
- import "package:async_redux/async_redux.dart" show Store;
1
+ import "package:async_redux/async_redux.dart" show Store, StoreProvider;
2
2
  import "package:flutter/material.dart";
3
3
  import "package:go_router/go_router.dart";
4
4
  import "package:only_bible_app/gen/l10n/app_localizations.dart";
@@ -7,6 +7,7 @@ import "package:only_bible_app/screens/book_select_screen.dart";
7
7
  import "package:only_bible_app/screens/chapter_select_screen.dart";
8
8
  import "package:only_bible_app/screens/chapter_view_screen.dart";
9
9
  import "package:only_bible_app/screens/webview_screen.dart";
10
+ import "package:only_bible_app/store/actions.dart";
10
11
  import "package:only_bible_app/store/app_navigator.dart";
11
12
  import "package:only_bible_app/store/app_state.dart";
12
13
  import "package:only_bible_app/theme.dart";
@@ -15,17 +16,16 @@ import "package:only_bible_app/utils.dart";
15
16
  class App extends StatelessWidget {
16
17
  final GlobalKey<NavigatorState> globalNavigatorKey;
17
18
  final Store<AppState> store;
18
- final AppNavigator navigator;
19
19
  late final GoRouter _router;
20
20
 
21
- App({super.key, required this.globalNavigatorKey, required this.store, required this.navigator}) {
21
+ App({super.key, required this.globalNavigatorKey, required this.store}) {
22
22
  final s = store.state;
23
23
  _router = GoRouter(
24
24
  navigatorKey: globalNavigatorKey,
25
25
  initialLocation:
26
26
  s.firstOpen ? "/bible" : "/chapter/${Uri.encodeComponent(s.bibleName)}/${s.savedBook}/${s.savedChapter}",
27
27
  redirect: (context, state) {
28
- navigator.hideActions();
28
+ store.dispatch(HideActionsAction());
29
29
  return null;
30
30
  },
31
31
  routes: [
@@ -107,20 +107,22 @@ class App extends StatelessWidget {
107
107
 
108
108
  @override
109
109
  Widget build(BuildContext context) {
110
- return AppNavigatorProvider(
110
+ return StoreProvider<AppState>(
111
111
  store: store,
112
+ child: AppRouterScope(
112
- navigator: navigator,
113
+ router: _router,
113
- child: Builder(
114
+ child: Builder(
114
- builder: (context) => MaterialApp.router(
115
+ builder: (context) => MaterialApp.router(
115
- routerConfig: _router,
116
+ routerConfig: _router,
116
- onGenerateTitle: (context) => context.l.title,
117
+ onGenerateTitle: (context) => context.l.title,
117
- localizationsDelegates: AppLocalizations.localizationsDelegates,
118
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
118
- supportedLocales: AppLocalizations.supportedLocales,
119
+ supportedLocales: AppLocalizations.supportedLocales,
119
- debugShowCheckedModeBanner: false,
120
+ debugShowCheckedModeBanner: false,
120
- themeMode: context.select((s) => s.darkMode) ? ThemeMode.dark : ThemeMode.light,
121
+ themeMode: context.select((s) => s.darkMode) ? ThemeMode.dark : ThemeMode.light,
121
- theme: lightTheme,
122
+ theme: lightTheme,
122
- darkTheme: darkTheme,
123
+ darkTheme: darkTheme,
123
- locale: Locale(context.select((s) => s.languageCode)),
124
+ locale: Locale(context.select((s) => s.languageCode)),
125
+ ),
124
126
  ),
125
127
  ),
126
128
  );
lib/dialog.dart CHANGED
@@ -1,7 +1,8 @@
1
1
  import "dart:ui";
2
2
  import "package:flutter/material.dart";
3
- import "package:only_bible_app/utils.dart";
3
+ import "package:only_bible_app/store/actions.dart";
4
4
  import "package:only_bible_app/store/app_navigator.dart";
5
+ import "package:only_bible_app/utils.dart";
5
6
 
6
7
  void showAlert(BuildContext context, String title, String message) {
7
8
  showDialog(
@@ -50,13 +51,13 @@ void showReportError(BuildContext context, String message, StackTrace? st) {
50
51
  TextButton(
51
52
  onPressed: () {
52
53
  recordError(message, st);
53
- context.nav.changeBible(context);
54
+ context.dispatch(ChangeBibleAction(context.router));
54
55
  },
55
56
  child: const Text("Yes"),
56
57
  ),
57
58
  TextButton(
58
59
  onPressed: () {
59
- context.nav.changeBible(context);
60
+ context.dispatch(ChangeBibleAction(context.router));
60
61
  },
61
62
  child: const Text("No"),
62
63
  ),
lib/gen/bible.gen.dart CHANGED
@@ -6,6 +6,8 @@ library only_bible_app.bible;
6
6
  import 'dart:typed_data' show Uint8List;
7
7
  import 'package:flat_buffers/flat_buffers.dart' as fb;
8
8
 
9
+
10
+
9
11
  class Verse {
10
12
  Verse._(this._bc, this._bcOffset);
11
13
  factory Verse(List<int> bytes) {
@@ -34,7 +36,8 @@ class _VerseReader extends fb.TableReader<Verse> {
34
36
  const _VerseReader();
35
37
 
36
38
  @override
37
- Verse createObject(fb.BufferContext bc, int offset) => Verse._(bc, offset);
39
+ Verse createObject(fb.BufferContext bc, int offset) =>
40
+ Verse._(bc, offset);
38
41
  }
39
42
 
40
43
  class VerseBuilder {
@@ -50,22 +53,18 @@ class VerseBuilder {
50
53
  fbBuilder.addInt32(0, index);
51
54
  return fbBuilder.offset;
52
55
  }
53
-
54
56
  int addBook(int? book) {
55
57
  fbBuilder.addInt32(1, book);
56
58
  return fbBuilder.offset;
57
59
  }
58
-
59
60
  int addChapter(int? chapter) {
60
61
  fbBuilder.addInt32(2, chapter);
61
62
  return fbBuilder.offset;
62
63
  }
63
-
64
64
  int addHeadingOffset(int? offset) {
65
65
  fbBuilder.addOffset(3, offset);
66
66
  return fbBuilder.offset;
67
67
  }
68
-
69
68
  int addTextOffset(int? offset) {
70
69
  fbBuilder.addOffset(4, offset);
71
70
  return fbBuilder.offset;
@@ -89,7 +88,8 @@ class VerseObjectBuilder extends fb.ObjectBuilder {
89
88
  int? chapter,
90
89
  String? heading,
91
90
  String? text,
91
+ })
92
- }) : _index = index,
92
+ : _index = index,
93
93
  _book = book,
94
94
  _chapter = chapter,
95
95
  _heading = heading,
@@ -98,8 +98,10 @@ class VerseObjectBuilder extends fb.ObjectBuilder {
98
98
  /// Finish building, and store into the [fbBuilder].
99
99
  @override
100
100
  int finish(fb.Builder fbBuilder) {
101
- final int? headingOffset = _heading == null ? null : fbBuilder.writeString(_heading!);
101
+ final int? headingOffset = _heading == null ? null
102
+ : fbBuilder.writeString(_heading!);
102
- final int? textOffset = _text == null ? null : fbBuilder.writeString(_text!);
103
+ final int? textOffset = _text == null ? null
104
+ : fbBuilder.writeString(_text!);
103
105
  fbBuilder.startTable(5);
104
106
  fbBuilder.addInt32(0, _index);
105
107
  fbBuilder.addInt32(1, _book);
@@ -117,7 +119,6 @@ class VerseObjectBuilder extends fb.ObjectBuilder {
117
119
  return fbBuilder.buffer;
118
120
  }
119
121
  }
120
-
121
122
  class Chapter {
122
123
  Chapter._(this._bc, this._bcOffset);
123
124
  factory Chapter(List<int> bytes) {
@@ -144,7 +145,8 @@ class _ChapterReader extends fb.TableReader<Chapter> {
144
145
  const _ChapterReader();
145
146
 
146
147
  @override
147
- Chapter createObject(fb.BufferContext bc, int offset) => Chapter._(bc, offset);
148
+ Chapter createObject(fb.BufferContext bc, int offset) =>
149
+ Chapter._(bc, offset);
148
150
  }
149
151
 
150
152
  class ChapterBuilder {
@@ -160,12 +162,10 @@ class ChapterBuilder {
160
162
  fbBuilder.addInt32(0, index);
161
163
  return fbBuilder.offset;
162
164
  }
163
-
164
165
  int addBook(int? book) {
165
166
  fbBuilder.addInt32(1, book);
166
167
  return fbBuilder.offset;
167
168
  }
168
-
169
169
  int addVersesOffset(int? offset) {
170
170
  fbBuilder.addOffset(2, offset);
171
171
  return fbBuilder.offset;
@@ -185,15 +185,16 @@ class ChapterObjectBuilder extends fb.ObjectBuilder {
185
185
  int? index,
186
186
  int? book,
187
187
  List<VerseObjectBuilder>? verses,
188
+ })
188
- }) : _index = index,
189
+ : _index = index,
189
190
  _book = book,
190
191
  _verses = verses;
191
192
 
192
193
  /// Finish building, and store into the [fbBuilder].
193
194
  @override
194
195
  int finish(fb.Builder fbBuilder) {
195
- final int? versesOffset =
196
+ final int? versesOffset = _verses == null ? null
196
- _verses == null ? null : fbBuilder.writeList(_verses!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
197
+ : fbBuilder.writeList(_verses!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
197
198
  fbBuilder.startTable(3);
198
199
  fbBuilder.addInt32(0, _index);
199
200
  fbBuilder.addInt32(1, _book);
@@ -209,7 +210,6 @@ class ChapterObjectBuilder extends fb.ObjectBuilder {
209
210
  return fbBuilder.buffer;
210
211
  }
211
212
  }
212
-
213
213
  class Book {
214
214
  Book._(this._bc, this._bcOffset);
215
215
  factory Book(List<int> bytes) {
@@ -235,7 +235,8 @@ class _BookReader extends fb.TableReader<Book> {
235
235
  const _BookReader();
236
236
 
237
237
  @override
238
- Book createObject(fb.BufferContext bc, int offset) => Book._(bc, offset);
238
+ Book createObject(fb.BufferContext bc, int offset) =>
239
+ Book._(bc, offset);
239
240
  }
240
241
 
241
242
  class BookBuilder {
@@ -251,7 +252,6 @@ class BookBuilder {
251
252
  fbBuilder.addInt32(0, index);
252
253
  return fbBuilder.offset;
253
254
  }
254
-
255
255
  int addChaptersOffset(int? offset) {
256
256
  fbBuilder.addOffset(1, offset);
257
257
  return fbBuilder.offset;
@@ -269,14 +269,15 @@ class BookObjectBuilder extends fb.ObjectBuilder {
269
269
  BookObjectBuilder({
270
270
  int? index,
271
271
  List<ChapterObjectBuilder>? chapters,
272
+ })
272
- }) : _index = index,
273
+ : _index = index,
273
274
  _chapters = chapters;
274
275
 
275
276
  /// Finish building, and store into the [fbBuilder].
276
277
  @override
277
278
  int finish(fb.Builder fbBuilder) {
278
- final int? chaptersOffset =
279
+ final int? chaptersOffset = _chapters == null ? null
279
- _chapters == null ? null : fbBuilder.writeList(_chapters!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
280
+ : fbBuilder.writeList(_chapters!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
280
281
  fbBuilder.startTable(2);
281
282
  fbBuilder.addInt32(0, _index);
282
283
  fbBuilder.addOffset(1, chaptersOffset);
@@ -291,7 +292,6 @@ class BookObjectBuilder extends fb.ObjectBuilder {
291
292
  return fbBuilder.buffer;
292
293
  }
293
294
  }
294
-
295
295
  class Bible {
296
296
  Bible._(this._bc, this._bcOffset);
297
297
  factory Bible(List<int> bytes) {
@@ -317,7 +317,8 @@ class _BibleReader extends fb.TableReader<Bible> {
317
317
  const _BibleReader();
318
318
 
319
319
  @override
320
- Bible createObject(fb.BufferContext bc, int offset) => Bible._(bc, offset);
320
+ Bible createObject(fb.BufferContext bc, int offset) =>
321
+ Bible._(bc, offset);
321
322
  }
322
323
 
323
324
  class BibleBuilder {
@@ -333,7 +334,6 @@ class BibleBuilder {
333
334
  fbBuilder.addOffset(0, offset);
334
335
  return fbBuilder.offset;
335
336
  }
336
-
337
337
  int addBooksOffset(int? offset) {
338
338
  fbBuilder.addOffset(1, offset);
339
339
  return fbBuilder.offset;
@@ -351,15 +351,17 @@ class BibleObjectBuilder extends fb.ObjectBuilder {
351
351
  BibleObjectBuilder({
352
352
  String? name,
353
353
  List<BookObjectBuilder>? books,
354
+ })
354
- }) : _name = name,
355
+ : _name = name,
355
356
  _books = books;
356
357
 
357
358
  /// Finish building, and store into the [fbBuilder].
358
359
  @override
359
360
  int finish(fb.Builder fbBuilder) {
360
- final int? nameOffset = _name == null ? null : fbBuilder.writeString(_name!);
361
+ final int? nameOffset = _name == null ? null
362
+ : fbBuilder.writeString(_name!);
361
- final int? booksOffset =
363
+ final int? booksOffset = _books == null ? null
362
- _books == null ? null : fbBuilder.writeList(_books!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
364
+ : fbBuilder.writeList(_books!.map((b) => b.getOrCreateOffset(fbBuilder)).toList());
363
365
  fbBuilder.startTable(2);
364
366
  fbBuilder.addOffset(0, nameOffset);
365
367
  fbBuilder.addOffset(1, booksOffset);
lib/main.dart CHANGED
@@ -1,17 +1,15 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:flutter/services.dart";
3
3
  import "package:flutter/foundation.dart";
4
- import "package:flutter/scheduler.dart";
5
4
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
6
5
  import "package:flutter_web_plugins/url_strategy.dart";
7
6
  import "package:flutter_native_splash/flutter_native_splash.dart";
8
7
  import "package:async_redux/async_redux.dart";
9
8
  import "package:only_bible_app/app.dart";
10
- import "package:only_bible_app/dialog.dart";
9
+
11
- import "package:only_bible_app/store/app_navigator.dart";
12
- import "package:only_bible_app/store/actions.dart";
13
10
  import "package:only_bible_app/store/app_persistor.dart";
14
11
  import "package:only_bible_app/store/app_state.dart";
12
+ import "package:only_bible_app/utils.dart";
15
13
 
16
14
  void updateStatusBar(bool v) {
17
15
  if (v) {
@@ -36,45 +34,26 @@ void updateStatusBar(bool v) {
36
34
  }
37
35
 
38
36
  void main() async {
39
- final globalNavigatorKey = GlobalKey<NavigatorState>();
40
- final persistor = AppPersistor();
41
- final initialState = await persistor.readState();
42
- final store = Store<AppState>(
43
- initialState: initialState ?? const AppState(),
44
- persistor: persistor,
45
- );
46
- // FlutterError.onError = (errorDetails) {
47
- // SchedulerBinding.instance.addPostFrameCallback((d) {
48
- // showReportError(
49
- // globalNavigatorKey.currentState!.context,
50
- // errorDetails.exception.toString(),
51
- // errorDetails.stack,
52
- // );
53
- // });
54
- // };
55
- // PlatformDispatcher.instance.onError = (error, stack) {
56
- // Future.delayed(const Duration(seconds: 1), () {
57
- // showReportError(
58
- // globalNavigatorKey.currentState!.context,
59
- // error.toString(),
60
- // stack,
61
- // );
62
- // });
63
- // return true;
64
- // };
65
37
  FlutterNativeSplash.preserve(
66
38
  widgetsBinding: WidgetsFlutterBinding.ensureInitialized(),
67
39
  );
68
40
  usePathUrlStrategy();
69
- WidgetsFlutterBinding.ensureInitialized();
70
41
  FlutterAzureTts.init(
71
42
  subscriptionKey: "a9d2d78796924a2a9df2b6d5c1c4a576",
72
43
  region: "centralindia",
73
44
  withLogs: true,
74
45
  );
46
+ final globalNavigatorKey = GlobalKey<NavigatorState>();
47
+ final persistor = AppPersistor();
48
+ final json = await persistor.readJson();
49
+ final bibleName = json?["bibleName"] as String? ?? "English";
50
+ final bible = await loadBible(bibleName);
51
+ final initialState = json != null ? AppState.fromJson(json, bible) : AppState(bible: bible);
52
+ final store = Store<AppState>(
53
+ initialState: initialState,
54
+ persistor: persistor,
55
+ );
75
56
  updateStatusBar(store.state.darkMode);
76
- await store.dispatchAndWait(LoadBibleAction());
77
- final navigator = AppNavigator(store);
78
- runApp(App(globalNavigatorKey: globalNavigatorKey, store: store, navigator: navigator));
57
+ runApp(App(globalNavigatorKey: globalNavigatorKey, store: store));
79
58
  FlutterNativeSplash.remove();
80
59
  }
lib/screens/bible_select_screen.dart CHANGED
@@ -1,6 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
- import "package:only_bible_app/store/app_navigator.dart";
3
2
  import "package:only_bible_app/store/actions.dart";
3
+ import "package:only_bible_app/store/app_navigator.dart";
4
4
  import "package:only_bible_app/utils.dart";
5
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
6
6
  import "package:only_bible_app/widgets/sliver_heading.dart";
@@ -38,12 +38,9 @@ class BibleSelectScreen extends StatelessWidget {
38
38
  if (s.firstOpen) {
39
39
  context.dispatch(FirstOpenDoneAction());
40
40
  }
41
+ context.dispatch(
41
- context.nav.updateCurrentBible(
42
+ UpdateCurrentBibleAction(
42
- context,
43
- l.languageTitle,
43
+ context.router, l.languageTitle, l.localeName, s.savedBook, s.savedChapter),
44
- l.localeName,
45
- s.savedBook,
46
- s.savedChapter,
47
44
  );
48
45
  },
49
46
  );
lib/screens/book_select_screen.dart CHANGED
@@ -1,5 +1,5 @@
1
1
  import "package:flutter/material.dart";
2
- import "package:go_router/go_router.dart";
2
+ import "package:only_bible_app/store/actions.dart";
3
3
  import "package:only_bible_app/store/app_navigator.dart";
4
4
  import "package:only_bible_app/utils.dart";
5
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
@@ -15,20 +15,14 @@ class BookSelectScreen extends StatelessWidget {
15
15
  dynamic onBookSelected(BuildContext context, Bible bible, int index) {
16
16
  final book = bible.books![index];
17
17
  if (book.chapters!.length == 1) {
18
- return context.nav.replaceBookChapter(context, bible.name!, index, 0);
18
+ return context.dispatch(ReplaceBookChapterAction(context.router, bible.name!, index, 0));
19
19
  }
20
- context.go("/chapters/${Uri.encodeComponent(bible.name!)}/${book.index}");
20
+ context.dispatch(ChangeChapterAction(context.router, bible, book));
21
21
  }
22
22
 
23
23
  @override
24
24
  Widget build(BuildContext context) {
25
25
  final bible = context.select((s) => s.bible);
26
- if (bible == null) {
27
- return ColoredBox(
28
- color: Theme.of(context).colorScheme.surface,
29
- child: const Center(child: CircularProgressIndicator()),
30
- );
31
- }
32
26
  return ScaffoldMenu(
33
27
  child: CustomScrollView(
34
28
  physics: const BouncingScrollPhysics(),
lib/screens/chapter_select_screen.dart CHANGED
@@ -1,4 +1,5 @@
1
1
  import "package:flutter/material.dart";
2
+ import "package:only_bible_app/store/actions.dart";
2
3
  import "package:only_bible_app/store/app_navigator.dart";
3
4
  import "package:only_bible_app/utils.dart";
4
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
@@ -14,12 +15,6 @@ class ChapterSelectScreen extends StatelessWidget {
14
15
  @override
15
16
  Widget build(BuildContext context) {
16
17
  final bible = context.select((s) => s.bible);
17
- if (bible == null) {
18
- return ColoredBox(
19
- color: Theme.of(context).colorScheme.surface,
20
- child: const Center(child: CircularProgressIndicator()),
21
- );
22
- }
23
18
  final book = bible.books![bookIndex];
24
19
  return ScaffoldMenu(
25
20
  child: CustomScrollView(
@@ -30,7 +25,8 @@ class ChapterSelectScreen extends StatelessWidget {
30
25
  children: List.generate(book.chapters!.length, (index) {
31
26
  return TextButton(
32
27
  child: Text("${index + 1}"),
28
+ onPressed: () =>
33
- onPressed: () => context.nav.replaceBookChapter(context, bible.name!, bookIndex, index),
29
+ context.dispatch(ReplaceBookChapterAction(context.router, bible.name!, bookIndex, index)),
34
30
  );
35
31
  }),
36
32
  ),
lib/screens/chapter_view_screen.dart CHANGED
@@ -18,12 +18,6 @@ class ChapterViewScreen extends StatelessWidget {
18
18
  @override
19
19
  Widget build(BuildContext context) {
20
20
  final bible = context.select((s) => s.bible);
21
- if (bible == null) {
22
- return ColoredBox(
23
- color: Theme.of(context).colorScheme.surface,
24
- child: const Center(child: CircularProgressIndicator()),
25
- );
26
- }
27
21
  final book = bible.books![bookIndex];
28
22
  final chapter = book.chapters![chapterIndex];
29
23
  return Scaffold(
lib/sheets/actions_sheet.dart CHANGED
@@ -1,6 +1,5 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/gen/bible.gen.dart";
3
- import "package:only_bible_app/store/app_navigator.dart";
4
3
  import "package:only_bible_app/store/actions.dart";
5
4
  import "package:only_bible_app/theme.dart";
6
5
  import "package:only_bible_app/utils.dart";
@@ -14,7 +13,7 @@ class ActionsSheet extends StatelessWidget {
14
13
  @override
15
14
  Widget build(BuildContext context) {
16
15
  final darkMode = context.select((s) => s.darkMode);
17
- final isPlaying = context.select((s) => s.isPlaying);
16
+ final isPlaying = context.isWaiting(TogglePlayAction);
18
17
  final iconColor = darkMode ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
19
18
  final audioIcon = isPlaying ? Icons.pause_circle_outline : Icons.play_circle_outline;
20
19
 
@@ -25,7 +24,7 @@ class ActionsSheet extends StatelessWidget {
25
24
  index,
26
25
  ),
27
26
  );
28
- context.nav.hideActions();
27
+ context.dispatch(HideActionsAction());
29
28
  }
30
29
 
31
30
  return Material(
@@ -46,7 +45,7 @@ class ActionsSheet extends StatelessWidget {
46
45
  List<Verse>.from(context.read().selectedVerses),
47
46
  ),
48
47
  );
49
- context.nav.hideActions();
48
+ context.dispatch(HideActionsAction());
50
49
  },
51
50
  icon: Icon(Icons.cancel_outlined, size: 28, color: iconColor),
52
51
  ),
@@ -73,13 +72,20 @@ class ActionsSheet extends StatelessWidget {
73
72
  IconButton(
74
73
  padding: EdgeInsets.zero,
75
74
  onPressed: () {
76
- context.read().onPlay(context, bible);
75
+ context.dispatch(TogglePlayAction(context, bible));
77
76
  },
78
77
  icon: Icon(audioIcon, size: 34, color: iconColor),
79
78
  ),
80
79
  IconButton(
81
80
  padding: EdgeInsets.zero,
81
+ onPressed: () {
82
- onPressed: () => context.nav.shareVerses(context, bible, context.read().selectedVerses),
82
+ final verses = context.read().selectedVerses;
83
+ context.dispatch(ShareVersesAction(
84
+ verses,
85
+ context.bookNames[verses.first.book],
86
+ context.read().languageCode,
87
+ ));
88
+ },
83
89
  icon: Icon(Icons.share_outlined, size: 34, color: iconColor),
84
90
  ),
85
91
  ],
lib/sheets/settings_sheet.dart CHANGED
@@ -1,7 +1,7 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/gen/bible.gen.dart";
3
- import "package:only_bible_app/store/app_navigator.dart";
4
3
  import "package:only_bible_app/store/actions.dart";
4
+ import "package:only_bible_app/store/app_navigator.dart";
5
5
  import "package:only_bible_app/utils.dart";
6
6
  import "package:settings_ui/settings_ui.dart";
7
7
 
@@ -36,7 +36,7 @@ class SettingsSheet extends StatelessWidget {
36
36
  leading: const Icon(Icons.book_outlined, color: Colors.blueAccent),
37
37
  title: Text(context.l.bibleTitle),
38
38
  value: Text(bible.name!),
39
- onPressed: context.nav.changeBible,
39
+ onPressed: (_) => context.dispatch(ChangeBibleAction(context.router)),
40
40
  ),
41
41
  SettingsTile.navigation(
42
42
  leading: const Icon(Icons.color_lens_outlined, color: Colors.green),
@@ -123,7 +123,7 @@ class SettingsSheet extends StatelessWidget {
123
123
  SettingsTile.navigation(
124
124
  leading: const Icon(Icons.policy_outlined, color: Colors.brown),
125
125
  title: Text(context.l.privacyPolicyTitle),
126
- onPressed: context.nav.showPrivacyPolicy,
126
+ onPressed: (_) => context.dispatch(ShowPrivacyPolicyAction(context.router)),
127
127
  ),
128
128
  SettingsTile.navigation(
129
129
  leading: const Icon(
@@ -131,17 +131,17 @@ class SettingsSheet extends StatelessWidget {
131
131
  color: Colors.blueGrey,
132
132
  ),
133
133
  title: Text(context.l.termsAndConditionsTitle),
134
- onPressed: context.nav.showTermsAndConditions,
134
+ onPressed: (_) => context.dispatch(ShowTermsAndConditionsAction(context.router)),
135
135
  ),
136
136
  SettingsTile.navigation(
137
137
  leading: const Icon(Icons.share_outlined, color: Colors.blueAccent),
138
138
  title: Text(context.l.shareAppTitle),
139
- onPressed: context.nav.shareAppLink,
139
+ onPressed: (_) => context.dispatch(ShareAppLinkAction()),
140
140
  ),
141
141
  SettingsTile.navigation(
142
142
  leading: Icon(Icons.star, color: Colors.yellowAccent.shade700),
143
143
  title: Text(context.l.rateAppTitle),
144
- onPressed: context.nav.rateApp,
144
+ onPressed: (_) => context.dispatch(RateAppAction()),
145
145
  ),
146
146
  SettingsTile.navigation(
147
147
  leading: Icon(
@@ -149,7 +149,7 @@ class SettingsSheet extends StatelessWidget {
149
149
  color: context.theme.colorScheme.onSurface,
150
150
  ),
151
151
  title: Text(context.l.aboutUsTitle),
152
- onPressed: context.nav.showAboutUs,
152
+ onPressed: (_) => context.dispatch(ShowAboutUsAction(context.router)),
153
153
  ),
154
154
  ],
155
155
  ),
lib/store/actions.dart CHANGED
@@ -1,18 +1,17 @@
1
+ import "dart:developer";
2
+
3
+ import "package:app_review/app_review.dart";
1
4
  import "package:async_redux/async_redux.dart";
5
+ import "package:flutter/material.dart";
6
+ import "package:go_router/go_router.dart";
7
+ import "package:only_bible_app/dialog.dart";
2
8
  import "package:only_bible_app/gen/bible.gen.dart";
9
+ import "package:only_bible_app/sheets/settings_sheet.dart";
3
10
  import "package:only_bible_app/store/app_state.dart";
11
+ import "package:only_bible_app/store/buffer_audio_source.dart";
4
12
  import "package:only_bible_app/utils.dart";
5
-
6
- class LoadBibleAction extends ReduxAction<AppState> {
7
- @override
8
- Future<AppState> reduce() async {
9
- if (state.bible != null && state.bible!.name == state.bibleName) {
13
+ import "package:only_bible_app/widgets/verses_view.dart";
10
- return state;
11
- }
12
- final bible = await loadBible(state.bibleName);
14
+ import "package:share_plus/share_plus.dart";
13
- return state.copy(bible: bible);
14
- }
15
- }
16
15
 
17
16
  class FirstOpenDoneAction extends ReduxAction<AppState> {
18
17
  @override
@@ -63,13 +62,45 @@ class UpdateChapterAction extends ReduxAction<AppState> {
63
62
  AppState reduce() => state.copy(savedBook: book, savedChapter: chapter);
64
63
  }
65
64
 
66
- class SetPlayingAction extends ReduxAction<AppState> {
65
+ class TogglePlayAction extends ReduxAction<AppState> {
66
+ final BuildContext buildContext;
67
- final bool value;
67
+ final Bible bible;
68
68
 
69
- SetPlayingAction(this.value);
69
+ TogglePlayAction(this.buildContext, this.bible);
70
70
 
71
71
  @override
72
+ Future<AppState?> reduce() async {
73
+ if (audioPlayer.playing) {
74
+ await audioPlayer.pause();
75
+ return null;
76
+ }
77
+ final versesToPlay = List<Verse>.from(state.selectedVerses);
78
+ final audioVoice = buildContext.currentLang.audioVoice;
79
+ final audioError = buildContext.l.audioError;
80
+ for (final v in versesToPlay) {
81
+ final pathname = "${bible.name!}_${v.book}_${v.chapter}_${v.index}";
82
+ try {
83
+ final data = await convertText(audioVoice, v.text ?? "");
84
+ await audioPlayer.setAudioSource(BufferAudioSource(data));
85
+ await audioPlayer.play();
86
+ await audioPlayer.stop();
87
+ } catch (err) {
88
+ log(
89
+ "Could not play audio",
90
+ name: "play",
91
+ error: (err.toString(), pathname),
92
+ );
72
- AppState reduce() => state.copy(isPlaying: value);
93
+ recordError((err.toString(), pathname).toString(), null);
94
+ if (buildContext.mounted) {
95
+ showError(buildContext, audioError);
96
+ }
97
+ return null;
98
+ } finally {
99
+ await audioPlayer.pause();
100
+ }
101
+ }
102
+ return null;
103
+ }
73
104
  }
74
105
 
75
106
  class SelectVerseAction extends ReduxAction<AppState> {
@@ -82,15 +113,12 @@ class SelectVerseAction extends ReduxAction<AppState> {
82
113
  final isSelected = state.selectedVerses.any(
83
114
  (el) => el.book == verse.book && el.chapter == verse.chapter && el.index == verse.index,
84
115
  );
85
- if (isSelected) {
116
+ final newVerses = isSelected
117
+ ? state.selectedVerses.where((it) => it.index != verse.index).toList()
118
+ : [...state.selectedVerses, verse];
86
- return state.copy(
119
+ return state.copy(
87
- selectedVerses: state.selectedVerses.where((it) => it.index != verse.index).toList(),
120
+ selectedVerses: newVerses,
88
- );
121
+ );
89
- } else {
90
- return state.copy(
91
- selectedVerses: [...state.selectedVerses, verse],
92
- );
93
- }
94
122
  }
95
123
  }
96
124
 
@@ -129,3 +157,302 @@ class RemoveHighlightAction extends ReduxAction<AppState> {
129
157
  return state.copy(highlights: updated);
130
158
  }
131
159
  }
160
+
161
+ // ─── Navigation actions ──────────────────────────────────────────────────────
162
+
163
+ class HideActionsAction extends ReduxAction<AppState> {
164
+ @override
165
+ AppState? reduce() {
166
+ return state.selectedVerses.isEmpty ? null : state.copy(selectedVerses: []);
167
+ }
168
+ }
169
+
170
+ class ShowSettingsAction extends ReduxAction<AppState> {
171
+ final BuildContext buildContext;
172
+ final Bible bible;
173
+
174
+ ShowSettingsAction(this.buildContext, this.bible);
175
+
176
+ @override
177
+ AppState? reduce() {
178
+ showModalBottomSheet(
179
+ context: buildContext,
180
+ isDismissible: true,
181
+ enableDrag: true,
182
+ showDragHandle: true,
183
+ useSafeArea: true,
184
+ builder: (context) => SettingsSheet(bible: bible),
185
+ );
186
+ return null;
187
+ }
188
+ }
189
+
190
+ class PushBookChapterAction extends ReduxAction<AppState> {
191
+ final GoRouter router;
192
+ final String bibleName;
193
+ final int book;
194
+ final int chapter;
195
+ final TextDirection? dir;
196
+
197
+ PushBookChapterAction(this.router, this.bibleName, this.book, this.chapter, [this.dir]);
198
+
199
+ @override
200
+ AppState reduce() {
201
+ audioPlayer.pause();
202
+ router.push(
203
+ "/chapter/${Uri.encodeComponent(bibleName)}/$book/$chapter",
204
+ extra: dir,
205
+ );
206
+ return state.copy(
207
+ savedBook: book,
208
+ savedChapter: chapter,
209
+ selectedVerses: [],
210
+ );
211
+ }
212
+ }
213
+
214
+ class ReplaceBookChapterAction extends ReduxAction<AppState> {
215
+ final GoRouter router;
216
+ final String bibleName;
217
+ final int book;
218
+ final int chapter;
219
+
220
+ ReplaceBookChapterAction(this.router, this.bibleName, this.book, this.chapter);
221
+
222
+ @override
223
+ AppState reduce() {
224
+ audioPlayer.pause();
225
+ router.go("/chapter/${Uri.encodeComponent(bibleName)}/$book/$chapter");
226
+ return state.copy(
227
+ savedBook: book,
228
+ savedChapter: chapter,
229
+ selectedVerses: [],
230
+ );
231
+ }
232
+ }
233
+
234
+ class NextChapterAction extends ReduxAction<AppState> {
235
+ final GoRouter router;
236
+ final Bible bible;
237
+ final int book;
238
+ final int chapter;
239
+
240
+ NextChapterAction(this.router, this.bible, this.book, this.chapter);
241
+
242
+ @override
243
+ AppState? reduce() {
244
+ final selectedBook = bible.books![book];
245
+ int? newBook;
246
+ int? newChapter;
247
+
248
+ if (selectedBook.chapters!.length > chapter + 1) {
249
+ newBook = selectedBook.index;
250
+ newChapter = chapter + 1;
251
+ } else if (selectedBook.index + 1 < bible.books!.length) {
252
+ final nextBook = bible.books![selectedBook.index + 1];
253
+ newBook = nextBook.index;
254
+ newChapter = 0;
255
+ }
256
+
257
+ if (newBook == null) return null;
258
+ audioPlayer.pause();
259
+ router.push(
260
+ "/chapter/${Uri.encodeComponent(bible.name!)}/$newBook/$newChapter",
261
+ extra: TextDirection.ltr,
262
+ );
263
+ return state.copy(
264
+ savedBook: newBook,
265
+ savedChapter: newChapter,
266
+ selectedVerses: [],
267
+ );
268
+ }
269
+ }
270
+
271
+ class PreviousChapterAction extends ReduxAction<AppState> {
272
+ final GoRouter router;
273
+ final Bible bible;
274
+ final int book;
275
+ final int chapter;
276
+
277
+ PreviousChapterAction(this.router, this.bible, this.book, this.chapter);
278
+
279
+ @override
280
+ AppState? reduce() {
281
+ final selectedBook = bible.books![book];
282
+ int? newBook;
283
+ int? newChapter;
284
+
285
+ if (chapter - 1 >= 0) {
286
+ newBook = selectedBook.index;
287
+ newChapter = chapter - 1;
288
+ } else if (selectedBook.index - 1 >= 0) {
289
+ final prevBook = bible.books![selectedBook.index - 1];
290
+ newBook = prevBook.index;
291
+ newChapter = prevBook.chapters!.length - 1;
292
+ }
293
+
294
+ if (newBook == null) return null;
295
+ audioPlayer.pause();
296
+ router.push(
297
+ "/chapter/${Uri.encodeComponent(bible.name!)}/$newBook/$newChapter",
298
+ extra: TextDirection.rtl,
299
+ );
300
+ return state.copy(
301
+ savedBook: newBook,
302
+ savedChapter: newChapter,
303
+ selectedVerses: [],
304
+ );
305
+ }
306
+ }
307
+
308
+ class ChangeBibleAction extends ReduxAction<AppState> {
309
+ final GoRouter router;
310
+
311
+ ChangeBibleAction(this.router);
312
+
313
+ @override
314
+ AppState? reduce() {
315
+ router.go("/bible");
316
+ return null;
317
+ }
318
+ }
319
+
320
+ class PushChangeBibleAction extends ReduxAction<AppState> {
321
+ final GoRouter router;
322
+
323
+ PushChangeBibleAction(this.router);
324
+
325
+ @override
326
+ AppState? reduce() {
327
+ router.push("/bible");
328
+ return null;
329
+ }
330
+ }
331
+
332
+ class ChangeBookAction extends ReduxAction<AppState> {
333
+ final GoRouter router;
334
+ final Bible bible;
335
+
336
+ ChangeBookAction(this.router, this.bible);
337
+
338
+ @override
339
+ AppState? reduce() {
340
+ router.push("/books/${Uri.encodeComponent(bible.name!)}");
341
+ return null;
342
+ }
343
+ }
344
+
345
+ class ChangeChapterAction extends ReduxAction<AppState> {
346
+ final GoRouter router;
347
+ final Bible bible;
348
+ final Book book;
349
+
350
+ ChangeChapterAction(this.router, this.bible, this.book);
351
+
352
+ @override
353
+ AppState? reduce() {
354
+ router.push("/chapters/${Uri.encodeComponent(bible.name!)}/${book.index}");
355
+ return null;
356
+ }
357
+ }
358
+
359
+ class UpdateCurrentBibleAction extends ReduxAction<AppState> {
360
+ final GoRouter router;
361
+ final String name;
362
+ final String code;
363
+ final int book;
364
+ final int chapter;
365
+
366
+ UpdateCurrentBibleAction(this.router, this.name, this.code, this.book, this.chapter);
367
+
368
+ @override
369
+ Future<AppState> reduce() async {
370
+ final bible = await loadBible(name);
371
+ router.go("/chapter/${Uri.encodeComponent(name)}/$book/$chapter");
372
+ return state.copy(
373
+ bibleName: name,
374
+ languageCode: code,
375
+ bible: bible,
376
+ savedBook: book,
377
+ savedChapter: chapter,
378
+ selectedVerses: [],
379
+ );
380
+ }
381
+ }
382
+
383
+ class ShowAboutUsAction extends ReduxAction<AppState> {
384
+ final GoRouter router;
385
+
386
+ ShowAboutUsAction(this.router);
387
+
388
+ @override
389
+ AppState? reduce() {
390
+ router.push("/webview", extra: "https://onlybible.app/about-us");
391
+ return null;
392
+ }
393
+ }
394
+
395
+ class ShowPrivacyPolicyAction extends ReduxAction<AppState> {
396
+ final GoRouter router;
397
+
398
+ ShowPrivacyPolicyAction(this.router);
399
+
400
+ @override
401
+ AppState? reduce() {
402
+ router.push("/webview", extra: "https://onlybible.app/privacy-policy");
403
+ return null;
404
+ }
405
+ }
406
+
407
+ class ShowTermsAndConditionsAction extends ReduxAction<AppState> {
408
+ final GoRouter router;
409
+
410
+ ShowTermsAndConditionsAction(this.router);
411
+
412
+ @override
413
+ AppState? reduce() {
414
+ router.push("/webview", extra: "https://onlybible.app/terms-and-conditions");
415
+ return null;
416
+ }
417
+ }
418
+
419
+ class ShareAppLinkAction extends ReduxAction<AppState> {
420
+ @override
421
+ AppState? reduce() {
422
+ final url = isAndroid()
423
+ ? "https://play.google.com/store/apps/details?id=sh.pyros.only_bible_app"
424
+ : "https://apps.apple.com/us/app/only-bible-app/id6467606465";
425
+ SharePlus.instance.share(ShareParams(subject: "Only Bible App", text: url));
426
+ return null;
427
+ }
428
+ }
429
+
430
+ class RateAppAction extends ReduxAction<AppState> {
431
+ @override
432
+ AppState? reduce() {
433
+ AppReview.requestReview;
434
+ return null;
435
+ }
436
+ }
437
+
438
+ class ShareVersesAction extends ReduxAction<AppState> {
439
+ final List<Verse> verses;
440
+ final String bookName;
441
+ final String languageCode;
442
+
443
+ ShareVersesAction(this.verses, this.bookName, this.languageCode);
444
+
445
+ @override
446
+ Future<AppState?> reduce() async {
447
+ final chapter = verses.first.chapter + 1;
448
+ final items = verses.sortedBy((e) => e.index).map((e) => e.index + 1);
449
+ final versesThrough = items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
450
+ final version = languageCode == "en" ? "KJV" : "";
451
+ final title = "$bookName $chapter:$versesThrough $version";
452
+ final text = verses.map((e) => e.text ?? "").join("\n");
453
+ await SharePlus.instance.share(
454
+ ShareParams(title: title, subject: title, text: "$title\n$text"),
455
+ );
456
+ return state.copy(selectedVerses: []);
457
+ }
458
+ }
lib/store/app_navigator.dart CHANGED
@@ -1,260 +1,21 @@
1
- import "package:async_redux/async_redux.dart" show Store, StoreProvider;
2
1
  import "package:flutter/material.dart";
3
- import "package:app_review/app_review.dart";
4
2
  import "package:go_router/go_router.dart";
5
- import "package:only_bible_app/gen/bible.gen.dart";
6
- import "package:only_bible_app/sheets/actions_sheet.dart";
7
- import "package:only_bible_app/sheets/settings_sheet.dart";
8
- import "package:only_bible_app/store/actions.dart";
9
- import "package:only_bible_app/store/app_state.dart";
10
- import "package:only_bible_app/utils.dart";
11
- import "package:share_plus/share_plus.dart";
12
3
 
13
- class AppNavigatorProvider extends StatelessWidget {
4
+ class AppRouterScope extends InheritedWidget {
14
- final Store<AppState> store;
15
- final AppNavigator navigator;
16
- final Widget child;
5
+ final GoRouter router;
17
6
 
18
- const AppNavigatorProvider({
7
+ const AppRouterScope({
19
8
  super.key,
20
- required this.store,
9
+ required this.router,
21
- required this.navigator,
22
- required this.child,
23
- });
24
-
25
- static AppNavigator of(BuildContext context) {
26
- return context.dependOnInheritedWidgetOfExactType<_InheritedAppNavigator>()!.navigator;
27
- }
28
-
29
- @override
30
- Widget build(BuildContext context) {
31
- return StoreProvider<AppState>(
32
- store: store,
33
- child: _InheritedAppNavigator(
34
- navigator: navigator,
35
- child: child,
36
- ),
37
- );
38
- }
39
- }
40
-
41
- class _InheritedAppNavigator extends InheritedWidget {
42
- final AppNavigator navigator;
43
-
44
- const _InheritedAppNavigator({
45
- required this.navigator,
46
10
  required super.child,
47
11
  });
48
12
 
49
- @override
50
- bool updateShouldNotify(_InheritedAppNavigator oldWidget) => false;
13
+ static AppRouterScope of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AppRouterScope>()!;
51
- }
52
14
 
53
- extension AppNavigatorContext on BuildContext {
15
+ @override
54
- AppNavigator get nav => AppNavigatorProvider.of(this);
16
+ bool updateShouldNotify(AppRouterScope old) => false;
55
17
  }
56
18
 
57
- class AppNavigator {
58
- final Store<AppState> store;
59
- OverlayEntry? _actionsOverlay;
60
-
61
- AppNavigator(this.store);
62
-
63
- String _chapterPath(String bibleName, int book, int chapter) {
64
- return "/chapter/${Uri.encodeComponent(bibleName)}/$book/$chapter";
65
- }
66
-
67
- void pushBookChapter(
68
- BuildContext context,
69
- String bibleName,
70
- int book,
71
- int chapter,
72
- TextDirection? dir,
73
- ) {
74
- store.dispatch(UpdateChapterAction(book, chapter));
75
- if (store.state.isPlaying) {
76
- AppState.player.pause();
77
- store.dispatch(SetPlayingAction(false));
78
- }
79
- hideActions();
80
- context.push(_chapterPath(bibleName, book, chapter), extra: dir);
81
- }
82
-
83
- void replaceBookChapter(
84
- BuildContext context,
85
- String bibleName,
86
- int book,
87
- int chapter,
88
- ) {
89
- store.dispatch(UpdateChapterAction(book, chapter));
90
- if (store.state.isPlaying) {
91
- AppState.player.pause();
92
- store.dispatch(SetPlayingAction(false));
93
- }
94
- hideActions();
95
- context.go(_chapterPath(bibleName, book, chapter));
96
- }
97
-
98
- void nextChapter(BuildContext context, Bible bible, int book, int chapter) {
99
- final selectedBook = bible.books![book];
100
- if (selectedBook.chapters!.length > chapter + 1) {
101
- pushBookChapter(
102
- context,
103
- bible.name!,
104
- selectedBook.index,
105
- chapter + 1,
106
- TextDirection.ltr,
107
- );
108
- } else {
109
- if (selectedBook.index + 1 < bible.books!.length) {
110
- final nextBook = bible.books![selectedBook.index + 1];
111
- pushBookChapter(
112
- context,
113
- bible.name!,
114
- nextBook.index,
115
- 0,
116
- TextDirection.ltr,
117
- );
118
- }
119
- }
120
- }
121
-
122
- void previousChapter(BuildContext context, Bible bible, int book, int chapter) {
123
- final selectedBook = bible.books![book];
124
- if (chapter - 1 >= 0) {
125
- pushBookChapter(
126
- context,
127
- bible.name!,
128
- selectedBook.index,
129
- chapter - 1,
130
- TextDirection.rtl,
131
- );
132
- } else {
133
- if (selectedBook.index - 1 >= 0) {
134
- final prevBook = bible.books![selectedBook.index - 1];
135
- pushBookChapter(
136
- context,
137
- bible.name!,
138
- prevBook.index,
139
- prevBook.chapters!.length - 1,
140
- TextDirection.rtl,
141
- );
142
- }
143
- }
144
- }
145
-
146
- void showAboutUs(BuildContext context) {
147
- context.push("/webview", extra: "https://onlybible.app/about-us");
148
- }
149
-
150
- void showPrivacyPolicy(BuildContext context) {
151
- context.push("/webview", extra: "https://onlybible.app/privacy-policy");
152
- }
153
-
154
- void showTermsAndConditions(BuildContext context) {
155
- context.push("/webview", extra: "https://onlybible.app/terms-and-conditions");
156
- }
157
-
158
- void changeBible(BuildContext context) {
159
- context.go("/bible");
160
- }
161
-
162
- void changeBibleFromHeader(BuildContext context) {
163
- context.push("/bible");
164
- }
165
-
166
- void changeBook(BuildContext context, Bible bible) {
167
- context.push("/books/${Uri.encodeComponent(bible.name!)}");
168
- }
169
-
170
- void changeChapter(BuildContext context, Bible bible, Book book, int index) {
171
- context.push("/chapters/${Uri.encodeComponent(bible.name!)}/${book.index}");
172
- }
173
-
174
- Future<void> updateCurrentBible(
175
- BuildContext context,
176
- String name,
177
- String code,
178
- int book,
179
- int chapter,
180
- ) async {
181
- hideActions();
182
- await store.dispatchAndWait(UpdateBibleAction(name, code));
183
- await store.dispatchAndWait(LoadBibleAction());
184
- context.go(_chapterPath(name, book, chapter));
185
- }
186
-
187
- void shareAppLink(BuildContext context) {
19
+ extension AppRouterContext on BuildContext {
188
- if (isAndroid()) {
189
- Share.share(
190
- subject: "Only Bible App",
191
- "https://play.google.com/store/apps/details?id=sh.pyros.only_bible_app",
192
- );
193
- } else {
194
- Share.share(
195
- subject: "Only Bible App",
196
- "https://apps.apple.com/us/app/only-bible-app/id6467606465",
197
- );
198
- }
199
- }
200
-
201
- Future<void> rateApp(BuildContext context) async {
202
- AppReview.requestReview;
203
- }
204
-
205
- Future<void> shareVerses(BuildContext context, Bible bible, List<Verse> verses) async {
206
- final name = context.bookNames[verses.first.book];
207
- final chapter = verses.first.chapter + 1;
208
- final items = verses.sortedBy((e) => e.index).map((e) => e.index + 1);
209
- final versesThrough = items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
210
- final version = context.currentLang.languageCode == "en" ? "KJV" : "";
211
- final title = "$name $chapter:$versesThrough $version";
212
- final text = verses.map((e) => e.text ?? "").join("\n");
213
- await SharePlus.instance.share(
214
- ShareParams(
215
- title: title,
216
- subject: title,
217
- text: "$title\n$text",
218
- ),
219
- );
220
- hideActions();
221
- }
222
-
223
- void showSettings(BuildContext context, Bible bible) {
224
- showModalBottomSheet(
225
- context: context,
226
- isDismissible: true,
227
- enableDrag: true,
228
- showDragHandle: true,
229
- useSafeArea: true,
230
- builder: (context) => SettingsSheet(bible: bible),
20
+ GoRouter get router => AppRouterScope.of(this).router;
231
- );
232
- }
233
-
234
- void showActions(BuildContext context, Bible bible) {
235
- _actionsOverlay?.remove();
236
- _actionsOverlay = null;
237
- final overlay = Overlay.of(context);
238
- _actionsOverlay = OverlayEntry(
239
- builder: (context) {
240
- final bottomPadding = MediaQuery.of(context).padding.bottom;
241
- return Align(
242
- alignment: Alignment.bottomCenter,
243
- child: Padding(
244
- padding: EdgeInsets.only(bottom: bottomPadding + 40, left: 20, right: 20),
245
- child: ActionsSheet(bible: bible),
246
- ),
247
- );
248
- },
249
- );
250
- overlay.insert(_actionsOverlay!);
251
- }
252
-
253
- void hideActions() {
254
- if (_actionsOverlay != null) {
255
- _actionsOverlay?.remove();
256
- _actionsOverlay = null;
257
- store.dispatch(ClearSelectedVersesAction());
258
- }
259
- }
260
21
  }
lib/store/app_persistor.dart CHANGED
@@ -15,12 +15,15 @@ class AppPersistor extends Persistor<AppState> {
15
15
 
16
16
  @override
17
17
  Future<AppState?> readState() async {
18
+ return null;
19
+ }
20
+
21
+ Future<Map<String, dynamic>?> readJson() async {
18
22
  try {
19
23
  final file = await _file;
20
24
  if (await file.exists()) {
21
25
  final contents = await file.readAsString();
22
- final json = jsonDecode(contents) as Map<String, dynamic>;
26
+ return jsonDecode(contents) as Map<String, dynamic>;
23
- return AppState.fromJson(json);
24
27
  }
25
28
  } catch (_) {}
26
29
  return null;
lib/store/app_state.dart CHANGED
@@ -1,17 +1,8 @@
1
- import "dart:developer";
2
1
  import "package:flutter/material.dart";
3
- import "package:just_audio/just_audio.dart";
4
- import "package:only_bible_app/dialog.dart";
5
2
  import "package:only_bible_app/gen/bible.gen.dart";
6
- import "package:only_bible_app/store/actions.dart";
7
- import "package:only_bible_app/store/buffer_audio_source.dart";
8
3
  import "package:only_bible_app/theme.dart";
9
- import "package:only_bible_app/utils.dart";
10
4
 
11
5
  class AppState {
12
- static final player = AudioPlayer();
13
- static final noteTextController = TextEditingController();
14
-
15
6
  final bool firstOpen;
16
7
  final String languageCode;
17
8
  final String bibleName;
@@ -21,12 +12,11 @@ class AppState {
21
12
  final double textScale;
22
13
  final int savedBook;
23
14
  final int savedChapter;
24
- final bool isPlaying;
25
15
  final List<Verse> selectedVerses;
26
16
  final Map<String, int> highlights;
27
- final Bible? bible;
17
+ final Bible bible;
28
18
 
29
- const AppState({
19
+ AppState({
30
20
  this.firstOpen = true,
31
21
  this.languageCode = "en",
32
22
  this.bibleName = "English",
@@ -36,10 +26,9 @@ class AppState {
36
26
  this.textScale = 0.0,
37
27
  this.savedBook = 0,
38
28
  this.savedChapter = 0,
39
- this.isPlaying = false,
40
29
  this.selectedVerses = const [],
41
30
  this.highlights = const {},
42
- this.bible,
31
+ required this.bible,
43
32
  });
44
33
 
45
34
  AppState copy({
@@ -52,7 +41,6 @@ class AppState {
52
41
  double? textScale,
53
42
  int? savedBook,
54
43
  int? savedChapter,
55
- bool? isPlaying,
56
44
  List<Verse>? selectedVerses,
57
45
  Map<String, int>? highlights,
58
46
  Bible? bible,
@@ -67,7 +55,6 @@ class AppState {
67
55
  textScale: textScale ?? this.textScale,
68
56
  savedBook: savedBook ?? this.savedBook,
69
57
  savedChapter: savedChapter ?? this.savedChapter,
70
- isPlaying: isPlaying ?? this.isPlaying,
71
58
  selectedVerses: selectedVerses ?? this.selectedVerses,
72
59
  highlights: highlights ?? this.highlights,
73
60
  bible: bible ?? this.bible,
@@ -87,7 +74,7 @@ class AppState {
87
74
  "highlights": highlights,
88
75
  };
89
76
 
90
- factory AppState.fromJson(Map<String, dynamic> json) => AppState(
77
+ factory AppState.fromJson(Map<String, dynamic> json, Bible bible) => AppState(
91
78
  firstOpen: json["firstOpen"] as bool? ?? true,
92
79
  languageCode: json["languageCode"] as String? ?? "en",
93
80
  bibleName: json["bibleName"] as String? ?? "English",
@@ -98,6 +85,7 @@ class AppState {
98
85
  savedBook: json["savedBook"] as int? ?? 0,
99
86
  savedChapter: json["savedChapter"] as int? ?? 0,
100
87
  highlights: (json["highlights"] as Map<String, dynamic>?)?.map((k, v) => MapEntry(k, v as int)) ?? const {},
88
+ bible: bible,
101
89
  );
102
90
 
103
91
  Color? getHighlight(Verse v) {
@@ -134,37 +122,4 @@ class AppState {
134
122
  backgroundColor: highlight ?? Theme.of(context).colorScheme.surface,
135
123
  );
136
124
  }
137
-
138
- Future<void> onPlay(BuildContext context, Bible bible) async {
139
- final versesToPlay = List<Verse>.from(context.read().selectedVerses);
140
- if (context.read().isPlaying) {
141
- await player.pause();
142
- context.dispatch(SetPlayingAction(false));
143
- } else {
144
- context.dispatch(SetPlayingAction(true));
145
- for (final v in versesToPlay) {
146
- final pathname = "${bible.name!}_${v.book}_${v.chapter}_${v.index}";
147
- try {
148
- final data = await convertText(context.currentLang.audioVoice, v.text ?? "");
149
- await player.setAudioSource(BufferAudioSource(data));
150
- await player.play();
151
- await player.stop();
152
- } catch (err) {
153
- log(
154
- "Could not play audio",
155
- name: "play",
156
- error: (err.toString(), pathname),
157
- );
158
- recordError((err.toString(), pathname).toString(), null);
159
- if (context.mounted) {
160
- showError(context, context.l.audioError);
161
- }
162
- return;
163
- } finally {
164
- await player.pause();
165
- context.dispatch(SetPlayingAction(false));
166
- }
167
- }
168
- }
169
- }
170
125
  }
lib/utils.dart CHANGED
@@ -1,6 +1,7 @@
1
1
  import "dart:convert";
2
2
  import "dart:io";
3
3
  import "package:async_redux/async_redux.dart";
4
+ export "package:async_redux/async_redux.dart" show BuildContextExtensionForProviderAndConnector;
4
5
  import "package:http/http.dart" as http;
5
6
  import "package:flutter/services.dart";
6
7
  import "package:package_info_plus/package_info_plus.dart";
@@ -45,9 +46,6 @@ extension AppStateExtension on BuildContext {
45
46
  AppState get state => getState<AppState>();
46
47
  AppState read() => getRead<AppState>();
47
48
  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);
48
- R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);
49
- void dispatch(ReduxAction<AppState> action) => StoreProvider.dispatch<AppState>(this, action);
50
- Future<void> dispatchAndWait(ReduxAction<AppState> action) => StoreProvider.dispatchAndWait<AppState>(this, action);
51
49
  }
52
50
 
53
51
  extension AppContext on BuildContext {
lib/widgets/chapter_app_bar.dart CHANGED
@@ -1,5 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/gen/bible.gen.dart";
3
+ import "package:only_bible_app/store/actions.dart";
3
4
  import "package:only_bible_app/store/app_navigator.dart";
4
5
  import "package:only_bible_app/utils.dart";
5
6
 
@@ -30,7 +31,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
30
31
  children: [
31
32
  InkWell(
32
33
  enableFeedback: true,
33
- onTap: () => context.nav.changeBook(context, bible),
34
+ onTap: () => context.dispatch(ChangeBookAction(context.router, bible)),
34
35
  child: Text(
35
36
  bookName,
36
37
  style: Theme.of(context).textTheme.headlineMedium,
@@ -39,7 +40,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
39
40
  ),
40
41
  InkWell(
41
42
  enableFeedback: true,
42
- onTap: () => context.nav.changeChapter(context, bible, book, book.index),
43
+ onTap: () => context.dispatch(ChangeChapterAction(context.router, bible, book)),
43
44
  child: Padding(
44
45
  padding: const EdgeInsets.only(left: 16),
45
46
  child: Text(
@@ -58,7 +59,7 @@ class ChapterAppBar extends StatelessWidget implements PreferredSizeWidget {
58
59
  child: IconButton(
59
60
  padding: EdgeInsets.zero,
60
61
  icon: const Icon(Icons.more_vert, size: 28),
61
- onPressed: () => context.nav.showSettings(context, bible),
62
+ onPressed: () => context.dispatch(ShowSettingsAction(context, bible)),
62
63
  ),
63
64
  ),
64
65
  ],
lib/widgets/verses_view.dart CHANGED
@@ -1,25 +1,20 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:flutter_swipe_detector/flutter_swipe_detector.dart";
3
+ import "package:just_audio/just_audio.dart";
3
4
  import "package:only_bible_app/gen/bible.gen.dart";
4
- import "package:only_bible_app/store/app_navigator.dart";
5
+ import "package:only_bible_app/sheets/actions_sheet.dart";
5
6
  import "package:only_bible_app/store/actions.dart";
7
+ import "package:only_bible_app/store/app_navigator.dart";
6
8
  import "package:only_bible_app/utils.dart";
7
9
 
10
+ final audioPlayer = AudioPlayer();
11
+
8
12
  class VersesView extends StatelessWidget {
9
13
  final Bible bible;
10
14
  final Chapter chapter;
11
15
 
12
16
  const VersesView({super.key, required this.bible, required this.chapter});
13
17
 
14
- void _onVerseTap(BuildContext context, Verse v) {
15
- context.dispatch(SelectVerseAction(v));
16
- if (context.read().selectedVerses.isNotEmpty) {
17
- context.nav.showActions(context, bible);
18
- } else {
19
- context.nav.hideActions();
20
- }
21
- }
22
-
23
18
  @override
24
19
  Widget build(BuildContext context) {
25
20
  final (boldFont, textScale, selectedVerses, _, _) =
@@ -28,45 +23,58 @@ class VersesView extends StatelessWidget {
28
23
  final textStyle = DefaultTextStyle.of(context).style;
29
24
  final theme = Theme.of(context).textTheme;
30
25
  final baseStyle = boldFont ? textStyle.copyWith(fontWeight: FontWeight.w500) : textStyle;
26
+ return Stack(
27
+ children: [
31
- return SwipeDetector(
28
+ SwipeDetector(
29
+ onSwipeLeft: (offset) =>
32
- onSwipeLeft: (offset) => context.nav.nextChapter(context, bible, chapter.book, chapter.index),
30
+ context.dispatch(NextChapterAction(context.router, bible, chapter.book, chapter.index)),
31
+ onSwipeRight: (offset) =>
33
- onSwipeRight: (offset) => context.nav.previousChapter(context, bible, chapter.book, chapter.index),
32
+ context.dispatch(PreviousChapterAction(context.router, bible, chapter.book, chapter.index)),
34
- child: SingleChildScrollView(
33
+ child: SingleChildScrollView(
35
- physics: const BouncingScrollPhysics(),
34
+ physics: const BouncingScrollPhysics(),
36
- padding: const EdgeInsets.symmetric(horizontal: 20),
35
+ padding: const EdgeInsets.symmetric(horizontal: 20),
37
- child: Column(
36
+ child: Column(
38
- crossAxisAlignment: CrossAxisAlignment.start,
37
+ crossAxisAlignment: CrossAxisAlignment.start,
39
- children: [
38
+ children: [
40
- for (final v in chapter.verses!)
39
+ for (final v in chapter.verses!)
41
- Padding(
40
+ Padding(
42
- padding: const EdgeInsets.only(bottom: 4),
41
+ padding: const EdgeInsets.only(bottom: 4),
43
- child: GestureDetector(
42
+ child: GestureDetector(
44
- onTap: () => _onVerseTap(context, v),
43
+ onTap: () => context.dispatch(SelectVerseAction(v)),
45
- behavior: HitTestBehavior.opaque,
44
+ behavior: HitTestBehavior.opaque,
46
- child: Text.rich(
45
+ child: Text.rich(
47
- TextSpan(
48
- style: baseStyle,
49
- children: [
50
- if (v.heading != null && v.heading!.isNotEmpty)
51
- TextSpan(
52
- text: "${v.heading!.replaceAll("<br>", "\n")}\n",
53
- style: theme.labelLarge,
54
- ),
55
- TextSpan(text: "${v.index + 1} ", style: theme.labelMedium),
56
46
  TextSpan(
47
+ style: baseStyle,
48
+ children: [
49
+ if (v.heading != null && v.heading!.isNotEmpty)
50
+ TextSpan(
51
+ text: "${v.heading!.replaceAll("<br>", "\n")}\n",
52
+ style: theme.labelLarge,
53
+ ),
54
+ TextSpan(text: "${v.index + 1} ", style: theme.labelMedium),
55
+ TextSpan(
57
- text: v.text ?? "",
56
+ text: v.text ?? "",
58
- style: appState.getHighlightStyle(context, v, false),
57
+ style: appState.getHighlightStyle(context, v, false),
58
+ ),
59
+ ],
59
60
  ),
61
+ textScaler: TextScaler.linear(1.1 + textScale / 2),
60
- ],
62
+ ),
61
63
  ),
62
- textScaler: TextScaler.linear(1.1 + textScale / 2),
63
64
  ),
65
+ if (selectedVerses.isNotEmpty) const SizedBox(height: 120),
66
+ ],
64
- ),
67
+ ),
65
- ),
68
+ ),
66
- if (selectedVerses.isNotEmpty) const SizedBox(height: 120),
67
- ],
68
69
  ),
70
+ if (selectedVerses.isNotEmpty)
71
+ Positioned(
72
+ left: 20,
73
+ right: 20,
74
+ bottom: MediaQuery.of(context).padding.bottom + 40,
75
+ child: Center(child: ActionsSheet(bible: bible)),
69
- ),
76
+ ),
77
+ ],
70
78
  );
71
79
  }
72
80
  }