~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
@@ -8,110 +8,13 @@ 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
10
  import "package:only_bible_app/store/app_state.dart";
11
- import "package:only_bible_app/store/app_persistor.dart";
12
11
  import "package:only_bible_app/theme.dart";
13
12
  import "package:only_bible_app/utils.dart";
14
13
 
15
- final globalNavigatorKey = GlobalKey<NavigatorState>();
16
- final persistor = AppPersistor();
17
- final Store<AppState> store = Store<AppState>(
18
- initialState: const AppState(),
19
- persistor: persistor,
20
- );
21
- final GoRouter router = buildRouter();
22
-
23
- GoRouter buildRouter() {
24
- final s = store.state;
25
- return GoRouter(
26
- navigatorKey: globalNavigatorKey,
27
- initialLocation: s.firstOpen
28
- ? "/bible"
29
- : "/chapter/${Uri.encodeComponent(s.bibleName)}/${s.savedBook}/${s.savedChapter}",
30
- routes: [
31
- GoRoute(
32
- path: "/bible",
33
- pageBuilder: (context, state) => const NoTransitionPage(
34
- child: BibleSelectScreen(),
35
- ),
36
- ),
37
- GoRoute(
38
- path: "/chapter/:bibleName/:bookIndex/:chapterIndex",
39
- pageBuilder: (context, state) {
40
- final bibleName =
41
- Uri.decodeComponent(state.pathParameters["bibleName"]!);
42
- final bookIndex = int.parse(state.pathParameters["bookIndex"]!);
43
- final chapterIndex = int.parse(state.pathParameters["chapterIndex"]!);
44
- final slideDir = state.extra as TextDirection?;
45
- if (slideDir != null) {
46
- return CustomTransitionPage(
47
- key: state.pageKey,
48
- child: ChapterViewScreen(
49
- bibleName: bibleName,
50
- bookIndex: bookIndex,
51
- chapterIndex: chapterIndex,
52
- ),
53
- transitionsBuilder:
54
- (context, animation, secondaryAnimation, child) {
55
- const begin = Offset(1.0, 0.0);
56
- const end = Offset.zero;
57
- const curve = Curves.ease;
58
- final tween = Tween(begin: begin, end: end)
59
- .chain(CurveTween(curve: curve));
60
- return SlideTransition(
61
- textDirection: slideDir,
62
- position: animation.drive(tween),
63
- child: child,
64
- );
65
- },
66
- );
67
- }
68
- return NoTransitionPage(
69
- key: state.pageKey,
70
- child: ChapterViewScreen(
71
- bibleName: bibleName,
72
- bookIndex: bookIndex,
73
- chapterIndex: chapterIndex,
74
- ),
75
- );
76
- },
77
- ),
78
- GoRoute(
79
- path: "/books/:bibleName",
80
- pageBuilder: (context, state) {
81
- final bibleName =
82
- Uri.decodeComponent(state.pathParameters["bibleName"]!);
83
- return NoTransitionPage(
84
- child: BookSelectScreen(bibleName: bibleName),
85
- );
86
- },
87
- ),
88
- GoRoute(
89
- path: "/chapters/:bibleName/:bookIndex",
90
- pageBuilder: (context, state) {
91
- final bibleName =
92
- Uri.decodeComponent(state.pathParameters["bibleName"]!);
93
- final bookIndex = int.parse(state.pathParameters["bookIndex"]!);
94
- return NoTransitionPage(
95
- child:
96
- ChapterSelectScreen(bibleName: bibleName, bookIndex: bookIndex),
97
- );
98
- },
99
- ),
100
- GoRoute(
101
- path: "/webview",
102
- pageBuilder: (context, state) {
103
- final url = state.extra as String;
104
- return NoTransitionPage(
105
- child: WebViewScreen(url: url),
106
- );
107
- },
108
- ),
109
- ],
110
- );
111
- }
112
-
113
14
  class App extends StatelessWidget {
15
+ final GlobalKey<NavigatorState> globalNavigatorKey;
114
- const App({super.key});
16
+ final Store<AppState> store;
17
+ const App({super.key, required this.globalNavigatorKey, required this.store});
115
18
 
116
19
  @override
117
20
  Widget build(BuildContext context) {
@@ -120,7 +23,86 @@ class App extends StatelessWidget {
120
23
  child: StoreConnector<AppState, _AppVm>(
121
24
  vm: () => _AppVmFactory(),
122
25
  builder: (context, vm) => MaterialApp.router(
123
- routerConfig: router,
26
+ routerConfig: GoRouter(
27
+ navigatorKey: globalNavigatorKey,
28
+ initialLocation: context.state.firstOpen
29
+ ? "/bible"
30
+ : "/chapter/${Uri.encodeComponent(context.state.bibleName)}/${context.state.savedBook}/${context.state.savedChapter}",
31
+ routes: [
32
+ GoRoute(
33
+ path: "/bible",
34
+ pageBuilder: (context, state) => const NoTransitionPage(
35
+ child: BibleSelectScreen(),
36
+ ),
37
+ ),
38
+ GoRoute(
39
+ path: "/chapter/:bibleName/:bookIndex/:chapterIndex",
40
+ pageBuilder: (context, state) {
41
+ final bibleName = Uri.decodeComponent(state.pathParameters["bibleName"]!);
42
+ final bookIndex = int.parse(state.pathParameters["bookIndex"]!);
43
+ final chapterIndex = int.parse(state.pathParameters["chapterIndex"]!);
44
+ final slideDir = state.extra as TextDirection?;
45
+ if (slideDir != null) {
46
+ return CustomTransitionPage(
47
+ key: state.pageKey,
48
+ child: ChapterViewScreen(
49
+ bibleName: bibleName,
50
+ bookIndex: bookIndex,
51
+ chapterIndex: chapterIndex,
52
+ ),
53
+ transitionsBuilder: (context, animation, secondaryAnimation, child) {
54
+ const begin = Offset(1.0, 0.0);
55
+ const end = Offset.zero;
56
+ const curve = Curves.ease;
57
+ final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
58
+ return SlideTransition(
59
+ textDirection: slideDir,
60
+ position: animation.drive(tween),
61
+ child: child,
62
+ );
63
+ },
64
+ );
65
+ }
66
+ return NoTransitionPage(
67
+ key: state.pageKey,
68
+ child: ChapterViewScreen(
69
+ bibleName: bibleName,
70
+ bookIndex: bookIndex,
71
+ chapterIndex: chapterIndex,
72
+ ),
73
+ );
74
+ },
75
+ ),
76
+ GoRoute(
77
+ path: "/books/:bibleName",
78
+ pageBuilder: (context, state) {
79
+ final bibleName = Uri.decodeComponent(state.pathParameters["bibleName"]!);
80
+ return NoTransitionPage(
81
+ child: BookSelectScreen(bibleName: bibleName),
82
+ );
83
+ },
84
+ ),
85
+ GoRoute(
86
+ path: "/chapters/:bibleName/:bookIndex",
87
+ pageBuilder: (context, state) {
88
+ final bibleName = Uri.decodeComponent(state.pathParameters["bibleName"]!);
89
+ final bookIndex = int.parse(state.pathParameters["bookIndex"]!);
90
+ return NoTransitionPage(
91
+ child: ChapterSelectScreen(bibleName: bibleName, bookIndex: bookIndex),
92
+ );
93
+ },
94
+ ),
95
+ GoRoute(
96
+ path: "/webview",
97
+ pageBuilder: (context, state) {
98
+ final url = state.extra as String;
99
+ return NoTransitionPage(
100
+ child: WebViewScreen(url: url),
101
+ );
102
+ },
103
+ ),
104
+ ],
105
+ ),
124
106
  onGenerateTitle: (context) => context.l.title,
125
107
  localizationsDelegates: AppLocalizations.localizationsDelegates,
126
108
  supportedLocales: AppLocalizations.supportedLocales,
lib/main.dart CHANGED
@@ -4,12 +4,24 @@ import "package:flutter/scheduler.dart";
4
4
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
5
5
  import "package:flutter_web_plugins/url_strategy.dart";
6
6
  import "package:flutter_native_splash/flutter_native_splash.dart";
7
+ import "package:async_redux/async_redux.dart";
7
8
  import "package:only_bible_app/app.dart";
8
9
  import "package:only_bible_app/dialog.dart";
9
10
  import "package:only_bible_app/env.dart";
10
11
  import "package:only_bible_app/navigation.dart";
12
+ import "package:only_bible_app/store/actions.dart";
13
+ import "package:only_bible_app/store/app_persistor.dart";
14
+ import "package:only_bible_app/store/app_state.dart";
15
+ import "package:only_bible_app/store/actions.dart";
11
16
 
12
17
  void main() async {
18
+ final globalNavigatorKey = GlobalKey<NavigatorState>();
19
+ final persistor = AppPersistor();
20
+ final initialState = await persistor.readState();
21
+ final store = Store<AppState>(
22
+ initialState: initialState ?? const AppState(),
23
+ persistor: persistor,
24
+ );
13
25
  // await store.persistor.readState();
14
26
  // FlutterError.onError = (errorDetails) {
15
27
  // SchedulerBinding.instance.addPostFrameCallback((d) {
@@ -41,6 +53,7 @@ void main() async {
41
53
  withLogs: true,
42
54
  );
43
55
  updateStatusBar(store.state.darkMode);
44
- runApp(const App());
56
+ await store.dispatchAndWait(LoadBibleAction());
57
+ runApp(App(globalNavigatorKey: globalNavigatorKey, store: store));
45
58
  // FlutterNativeSplash.remove();
46
59
  }
lib/navigation.dart CHANGED
@@ -7,7 +7,6 @@ import "package:only_bible_app/sheets/actions_sheet.dart";
7
7
  import "package:only_bible_app/sheets/settings_sheet.dart";
8
8
  import "package:only_bible_app/store/actions.dart";
9
9
  import "package:only_bible_app/store/app_state.dart";
10
- import "package:only_bible_app/app.dart";
11
10
  import "package:only_bible_app/utils.dart";
12
11
  import "package:share_plus/share_plus.dart";
13
12
 
@@ -44,10 +43,10 @@ void pushBookChapter(
44
43
  int chapter,
45
44
  TextDirection? dir,
46
45
  ) {
47
- store.dispatch(UpdateChapterAction(book, chapter));
46
+ context.dispatch(UpdateChapterAction(book, chapter));
48
- if (store.state.isPlaying) {
47
+ if (context.state.isPlaying) {
49
48
  AppState.player.pause();
50
- store.dispatch(SetPlayingAction(false));
49
+ context.dispatch(SetPlayingAction(false));
51
50
  }
52
51
  hideActions(context);
53
52
  context.push(_chapterPath(bibleName, book, chapter), extra: dir);
@@ -59,10 +58,10 @@ void replaceBookChapter(
59
58
  int book,
60
59
  int chapter,
61
60
  ) {
62
- store.dispatch(UpdateChapterAction(book, chapter));
61
+ context.dispatch(UpdateChapterAction(book, chapter));
63
- if (store.state.isPlaying) {
62
+ if (context.state.isPlaying) {
64
63
  AppState.player.pause();
65
- store.dispatch(SetPlayingAction(false));
64
+ context.dispatch(SetPlayingAction(false));
66
65
  }
67
66
  hideActions(context);
68
67
  context.go(_chapterPath(bibleName, book, chapter));
@@ -156,7 +155,8 @@ Future<void> updateCurrentBible(
156
155
  int chapter,
157
156
  ) async {
158
157
  hideActions(context);
159
- store.dispatch(UpdateBibleAction(name, code));
158
+ context.dispatch(UpdateBibleAction(name, code));
159
+ await context.dispatchAndWait(LoadBibleAction());
160
160
  context.go(_chapterPath(name, book, chapter));
161
161
  }
162
162
 
@@ -178,13 +178,11 @@ Future<void> rateApp(BuildContext context) async {
178
178
  AppReview.requestReview;
179
179
  }
180
180
 
181
- Future<void> shareVerses(
182
- BuildContext context, Bible bible, List<Verse> verses) async {
181
+ Future<void> shareVerses(BuildContext context, Bible bible, List<Verse> verses) async {
183
182
  final name = context.bookNames[verses.first.book];
184
183
  final chapter = verses.first.chapter + 1;
185
184
  final items = verses.sortedBy((e) => e.index).map((e) => e.index + 1);
186
- final versesThrough =
187
- items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
185
+ final versesThrough = items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
188
186
  final version = context.currentLang.languageCode == "en" ? "KJV" : "";
189
187
  final title = "$name $chapter:$versesThrough $version";
190
188
  final text = verses.map((e) => e.text).join("\n");
@@ -209,18 +207,26 @@ void showSettings(BuildContext context, Bible bible) {
209
207
  );
210
208
  }
211
209
 
212
- PersistentBottomSheetController? _actionsController;
210
+ OverlayEntry? _actionsOverlay;
213
211
 
214
212
  void showActions(BuildContext context, Bible bible) {
213
+ _actionsOverlay?.remove();
214
+ _actionsOverlay = null;
215
- hideActions(context);
215
+ final overlay = Overlay.of(context);
216
- _actionsController = showBottomSheet(
216
+ _actionsOverlay = OverlayEntry(
217
+ builder: (context) => Align(
218
+ alignment: Alignment.bottomCenter,
217
- context: context,
219
+ child: Padding(
220
+ padding: const EdgeInsets.only(bottom: 40, left: 20, right: 20),
218
- builder: (context) => ActionsSheet(bible: bible),
221
+ child: ActionsSheet(bible: bible),
222
+ ),
223
+ ),
219
224
  );
225
+ overlay.insert(_actionsOverlay!);
220
226
  }
221
227
 
222
228
  void hideActions(BuildContext context) {
223
- _actionsController?.close();
229
+ _actionsOverlay?.remove();
224
- _actionsController = null;
230
+ _actionsOverlay = null;
225
- store.dispatch(ClearSelectedVersesAction());
231
+ context.dispatch(ClearSelectedVersesAction());
226
232
  }
lib/screens/bible_select_screen.dart CHANGED
@@ -1,7 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/navigation.dart";
3
3
  import "package:only_bible_app/store/actions.dart";
4
- import "package:only_bible_app/app.dart";
5
4
  import "package:only_bible_app/utils.dart";
6
5
  import "package:only_bible_app/widgets/scaffold_menu.dart";
7
6
  import "package:only_bible_app/widgets/sliver_heading.dart";
@@ -18,7 +17,7 @@ class BibleSelectScreen extends StatelessWidget {
18
17
  slivers: [
19
18
  SliverHeading(
20
19
  title: context.l.bibleSelectTitle,
21
- showClose: !store.state.firstOpen,
20
+ showClose: !context.state.firstOpen,
22
21
  ),
23
22
  SliverTileGrid(
24
23
  listType: ListType.extraLarge,
@@ -35,15 +34,15 @@ class BibleSelectScreen extends StatelessWidget {
35
34
  )
36
35
  : Text(l.languageTitle, textScaleFactor: 1.1),
37
36
  onPressed: () {
38
- if (store.state.firstOpen) {
37
+ if (context.state.firstOpen) {
39
- store.dispatch(FirstOpenDoneAction());
38
+ context.dispatch(FirstOpenDoneAction());
40
39
  }
41
40
  updateCurrentBible(
42
41
  context,
43
42
  l.languageTitle,
44
43
  l.localeName,
45
- store.state.savedBook,
44
+ context.state.savedBook,
46
- store.state.savedChapter,
45
+ context.state.savedChapter,
47
46
  );
48
47
  },
49
48
  );
lib/screens/book_select_screen.dart CHANGED
@@ -22,57 +22,41 @@ class BookSelectScreen extends StatelessWidget {
22
22
 
23
23
  @override
24
24
  Widget build(BuildContext context) {
25
- return FutureBuilder(
26
- future: loadBible(bibleName),
27
- builder: (context, state) {
25
+ final bible = context.state.bible;
26
+ if (bible == null) {
28
- return state.when(
27
+ return ColoredBox(
29
- loading: () => ColoredBox(
30
- color: Theme.of(context).colorScheme.surface,
28
+ color: Theme.of(context).colorScheme.surface,
31
- child: const Center(child: CircularProgressIndicator()),
29
+ child: const Center(child: CircularProgressIndicator()),
30
+ );
31
+ }
32
+ return ScaffoldMenu(
33
+ child: CustomScrollView(
34
+ physics: const BouncingScrollPhysics(),
35
+ slivers: [
36
+ SliverHeading(title: context.l.oldTestamentTitle, showClose: true),
37
+ SliverTileGrid(
38
+ children: List.of(
39
+ bible.getOldBooks().map((book) {
40
+ return TextButton(
41
+ child: Text(book.shortName(context.bookNames[book.index])),
42
+ onPressed: () => onBookSelected(context, bible, book.index),
43
+ );
44
+ }),
45
+ ),
32
46
  ),
33
- success: (Bible? bible) {
34
- return ScaffoldMenu(
35
- child: CustomScrollView(
36
- physics: const BouncingScrollPhysics(),
37
- slivers: [
38
- SliverHeading(
39
- title: context.l.oldTestamentTitle, showClose: true),
47
+ SliverHeading(title: context.l.newTestamentTitle, top: 30, bottom: 20),
40
- SliverTileGrid(
48
+ SliverTileGrid(
41
- children: List.of(
49
+ children: List.of(
42
- bible!.getOldBooks().map((book) {
50
+ bible.getNewBooks().map((book) {
43
- return TextButton(
51
+ return TextButton(
44
- child: Text(
45
- book.shortName(context.bookNames[book.index])),
52
+ child: Text(book.shortName(context.bookNames[book.index])),
46
- onPressed: () =>
47
- onBookSelected(context, bible, book.index),
53
+ onPressed: () => onBookSelected(context, bible, book.index),
48
- );
54
+ );
49
- }),
55
+ }),
50
- ),
56
+ ),
51
- ),
52
- SliverHeading(
53
- title: context.l.newTestamentTitle, top: 30, bottom: 20),
54
- SliverTileGrid(
55
- children: List.of(
56
- bible.getNewBooks().map((book) {
57
- return TextButton(
58
- child: Text(
59
- book.shortName(context.bookNames[book.index])),
60
- onPressed: () =>
61
- onBookSelected(context, bible, book.index),
62
- );
63
- }),
64
- ),
65
- ),
66
- ],
67
- ),
68
- );
69
- },
70
- error: () => ColoredBox(
71
- color: Theme.of(context).colorScheme.surface,
72
- child: Center(child: Text("Could not load bible ${state.error}")),
73
57
  ),
58
+ ],
74
- );
59
+ ),
75
- },
76
60
  );
77
61
  }
78
62
  }
lib/screens/chapter_select_screen.dart CHANGED
@@ -1,5 +1,4 @@
1
1
  import "package:flutter/material.dart";
2
- import "package:only_bible_app/models.dart";
3
2
  import "package:only_bible_app/navigation.dart";
4
3
  import "package:only_bible_app/utils.dart";
5
4
  import "package:only_bible_app/widgets/scaffold_menu.dart";
@@ -10,46 +9,33 @@ class ChapterSelectScreen extends StatelessWidget {
10
9
  final String bibleName;
11
10
  final int bookIndex;
12
11
 
13
- const ChapterSelectScreen(
14
- {super.key, required this.bibleName, required this.bookIndex});
12
+ const ChapterSelectScreen({super.key, required this.bibleName, required this.bookIndex});
15
13
 
16
14
  @override
17
15
  Widget build(BuildContext context) {
18
- return FutureBuilder(
19
- future: loadBible(bibleName),
20
- builder: (context, state) {
16
+ final bible = context.state.bible;
17
+ if (bible == null) {
21
- return state.when(
18
+ return ColoredBox(
22
- loading: () => ColoredBox(
23
- color: Theme.of(context).colorScheme.surface,
19
+ color: Theme.of(context).colorScheme.surface,
24
- child: const Center(child: CircularProgressIndicator()),
20
+ child: const Center(child: CircularProgressIndicator()),
21
+ );
22
+ }
23
+ final book = bible.books[bookIndex];
24
+ return ScaffoldMenu(
25
+ child: CustomScrollView(
26
+ physics: const BouncingScrollPhysics(),
27
+ slivers: [
28
+ SliverHeading(title: context.bookNames[book.index], showClose: true),
29
+ SliverTileGrid(
30
+ children: List.generate(book.chapters.length, (index) {
31
+ return TextButton(
32
+ child: Text("${index + 1}"),
33
+ onPressed: () => replaceBookChapter(context, bible.name, bookIndex, index),
34
+ );
35
+ }),
25
36
  ),
26
- success: (Bible? bible) {
27
- final book = bible!.books[bookIndex];
28
- return ScaffoldMenu(
29
- child: CustomScrollView(
30
- physics: const BouncingScrollPhysics(),
31
- slivers: [
32
- SliverHeading(
33
- title: context.bookNames[book.index], showClose: true),
34
- SliverTileGrid(
35
- children: List.generate(book.chapters.length, (index) {
36
- return TextButton(
37
- child: Text("${index + 1}"),
38
- onPressed: () => replaceBookChapter(
39
- context, bible.name, bookIndex, index),
40
- );
41
- }),
37
+ ],
42
- ),
38
+ ),
43
- ],
44
- ),
45
- );
46
- },
47
- error: () => ColoredBox(
48
- color: Theme.of(context).colorScheme.surface,
49
- child: Center(child: Text("Could not load bible ${state.error}")),
50
- ),
51
- );
52
- },
53
39
  );
54
40
  }
55
41
  }
lib/screens/chapter_view_screen.dart CHANGED
@@ -1,6 +1,4 @@
1
1
  import "package:flutter/material.dart";
2
- import "package:only_bible_app/models.dart";
3
- import "package:only_bible_app/store/actions.dart";
4
2
  import "package:only_bible_app/utils.dart";
5
3
  import "package:only_bible_app/widgets/chapter_app_bar.dart";
6
4
  import "package:only_bible_app/widgets/verses_view.dart";
@@ -19,42 +17,25 @@ class ChapterViewScreen extends StatelessWidget {
19
17
 
20
18
  @override
21
19
  Widget build(BuildContext context) {
22
- return FutureBuilder(
23
- future: loadBible(bibleName),
24
- builder: (context, state) {
20
+ final bible = context.state.bible;
21
+ if (bible == null) {
25
- return state.when(
22
+ return ColoredBox(
26
- loading: () => ColoredBox(
27
- color: Theme.of(context).colorScheme.surface,
23
+ color: Theme.of(context).colorScheme.surface,
28
- child: const Center(
29
- child: CircularProgressIndicator(),
24
+ child: const Center(child: CircularProgressIndicator()),
25
+ );
26
+ }
27
+ final book = bible.books[bookIndex];
28
+ final chapter = book.chapters[chapterIndex];
29
+ return Scaffold(
30
+ appBar: ChapterAppBar(bible: bible, book: book, chapter: chapter),
31
+ backgroundColor: Theme.of(context).colorScheme.surface,
32
+ body: SafeArea(
33
+ child: Column(
34
+ children: [
35
+ Expanded(child: VersesView(bible: bible, chapter: chapter)),
36
+ ],
30
- ),
37
+ ),
31
- ),
38
+ ),
32
- success: (Bible? bible) {
33
- final book = bible!.books[bookIndex];
34
- final chapter = book.chapters[chapterIndex];
35
- return Scaffold(
36
- appBar: ChapterAppBar(bible: bible, book: book, chapter: chapter),
37
- backgroundColor: Theme.of(context).colorScheme.surface,
38
- body: SafeArea(
39
- child: Column(
40
- children: [
41
- Expanded(child: VersesView(bible: bible, chapter: chapter)),
42
- ],
43
- ),
44
- ),
45
- );
46
- },
47
- error: () {
48
- print(state.stackTrace);
49
- return ColoredBox(
50
- color: Theme.of(context).colorScheme.surface,
51
- child: Center(
52
- child: Text("Could not load the bible ${state.error}"),
53
- ),
54
- );
55
- },
56
- );
57
- },
58
39
  );
59
40
  }
60
41
  }
lib/sheets/actions_sheet.dart CHANGED
@@ -1,7 +1,6 @@
1
1
  import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/models.dart";
3
3
  import "package:only_bible_app/navigation.dart";
4
- import "package:only_bible_app/app.dart";
5
4
  import "package:only_bible_app/store/actions.dart";
6
5
  import "package:only_bible_app/theme.dart";
7
6
  import "package:only_bible_app/utils.dart";
@@ -14,95 +13,75 @@ class ActionsSheet extends StatelessWidget {
14
13
 
15
14
  @override
16
15
  Widget build(BuildContext context) {
17
- final iconColor = store.state.darkMode
16
+ final iconColor = context.state.darkMode ? Colors.white.withOpacity(0.9) : Colors.black.withOpacity(0.9);
18
- ? Colors.white.withOpacity(0.9)
19
- : Colors.black.withOpacity(0.9);
20
- final audioIcon = context.state.isPlaying
17
+ final audioIcon = context.state.isPlaying ? Icons.pause_circle_outline : Icons.play_circle_outline;
21
- ? Icons.pause_circle_outline
22
- : Icons.play_circle_outline;
23
18
 
24
19
  void onHighlight(int index) {
20
+ context.dispatch(
25
- store.dispatch(SetHighlightAction(
21
+ SetHighlightAction(
26
- List<Verse>.from(store.state.selectedVerses),
22
+ List<Verse>.from(context.state.selectedVerses),
27
- index,
23
+ index,
24
+ ),
28
- ));
25
+ );
29
26
  hideActions(context);
30
27
  }
31
28
 
32
- return Container(
29
+ return Material(
33
- height: 50,
30
+ elevation: 4,
31
+ borderRadius: BorderRadius.circular(28),
34
32
  color: Theme.of(context).colorScheme.surface,
33
+ child: Container(
34
+ height: 50,
35
- padding: const EdgeInsets.only(left: 20, right: 20),
35
+ padding: const EdgeInsets.only(left: 8, right: 8),
36
- child: Row(
36
+ child: Row(
37
- mainAxisAlignment: MainAxisAlignment.spaceAround,
37
+ mainAxisSize: MainAxisSize.min,
38
- children: [
38
+ children: [
39
- IconButton(
39
+ IconButton(
40
- padding: EdgeInsets.zero,
40
+ padding: EdgeInsets.zero,
41
- onPressed: () {
41
+ onPressed: () {
42
- store.dispatch(
42
+ context.dispatch(
43
- RemoveHighlightAction(
43
+ RemoveHighlightAction(
44
- List<Verse>.from(store.state.selectedVerses),
44
+ List<Verse>.from(context.state.selectedVerses),
45
- ),
45
+ ),
46
- );
46
+ );
47
- hideActions(context);
47
+ hideActions(context);
48
- },
48
+ },
49
- icon: Icon(Icons.cancel_outlined, size: 28, color: iconColor),
49
+ icon: Icon(Icons.cancel_outlined, size: 28, color: iconColor),
50
- ),
50
+ ),
51
- if (context.state.selectedVerses
52
- .any((v) => context.state.getHighlight(v) != null)) ...[
53
51
  HighlightButton(
54
52
  index: 0,
55
- color:
56
- store.state.darkMode ? darkHighlights[0] : lightHighlights[0],
53
+ color: context.state.darkMode ? darkHighlights[0] : lightHighlights[0],
57
54
  onHighlightSelected: onHighlight,
58
55
  ),
59
56
  HighlightButton(
60
57
  index: 1,
61
- color:
62
- store.state.darkMode ? darkHighlights[1] : lightHighlights[1],
58
+ color: context.state.darkMode ? darkHighlights[1] : lightHighlights[1],
63
59
  onHighlightSelected: onHighlight,
64
60
  ),
65
61
  HighlightButton(
66
62
  index: 2,
67
- color:
68
- store.state.darkMode ? darkHighlights[2] : lightHighlights[2],
63
+ color: context.state.darkMode ? darkHighlights[2] : lightHighlights[2],
69
64
  onHighlightSelected: onHighlight,
70
65
  ),
71
66
  HighlightButton(
72
67
  index: 3,
73
- color:
74
- store.state.darkMode ? darkHighlights[3] : lightHighlights[3],
68
+ color: context.state.darkMode ? darkHighlights[3] : lightHighlights[3],
75
69
  onHighlightSelected: onHighlight,
76
70
  ),
77
- ] else ...[
78
- IconButton(
79
- padding: EdgeInsets.zero,
80
- onPressed: () {
81
- store.dispatch(
82
- SetHighlightAction(
83
- List<Verse>.from(store.state.selectedVerses),
84
- 0,
85
- ),
86
- );
87
- },
88
- icon:
89
- Icon(Icons.border_color_outlined, size: 28, color: iconColor),
90
- ),
91
71
  IconButton(
92
72
  padding: EdgeInsets.zero,
93
73
  onPressed: () {
94
- store.state.onPlay(context, bible);
74
+ context.state.onPlay(context, bible);
95
75
  },
96
76
  icon: Icon(audioIcon, size: 34, color: iconColor),
97
77
  ),
98
78
  IconButton(
99
79
  padding: EdgeInsets.zero,
100
- onPressed: () =>
101
- shareVerses(context, bible, store.state.selectedVerses),
80
+ onPressed: () => shareVerses(context, bible, context.state.selectedVerses),
102
81
  icon: Icon(Icons.share_outlined, size: 34, color: iconColor),
103
82
  ),
104
83
  ],
105
- ],
84
+ ),
106
85
  ),
107
86
  );
108
87
  }
lib/sheets/settings_sheet.dart CHANGED
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
2
2
  import "package:only_bible_app/models.dart";
3
3
  import "package:only_bible_app/navigation.dart";
4
4
  import "package:only_bible_app/store/actions.dart";
5
- import "package:only_bible_app/app.dart";
6
5
  import "package:only_bible_app/utils.dart";
7
6
  import "package:settings_ui/settings_ui.dart";
8
7
 
@@ -31,26 +30,22 @@ class SettingsSheet extends StatelessWidget {
31
30
  margin: const EdgeInsetsDirectional.symmetric(horizontal: 20),
32
31
  tiles: [
33
32
  SettingsTile.navigation(
34
- leading:
35
- const Icon(Icons.book_outlined, color: Colors.blueAccent),
33
+ leading: const Icon(Icons.book_outlined, color: Colors.blueAccent),
36
34
  title: Text(context.l.bibleTitle),
37
35
  value: Text(bible.name),
38
36
  onPressed: changeBible,
39
37
  ),
40
38
  SettingsTile.navigation(
41
- leading:
42
- const Icon(Icons.color_lens_outlined, color: Colors.green),
39
+ leading: const Icon(Icons.color_lens_outlined, color: Colors.green),
43
40
  title: Text(context.l.themeTitle),
44
41
  trailing: ToggleButtons(
45
42
  onPressed: (int index) {
46
- store.dispatch(ToggleDarkModeAction());
43
+ context.dispatch(ToggleDarkModeAction());
47
44
  },
48
45
  highlightColor: Colors.transparent,
49
46
  borderColor: Colors.grey,
50
47
  borderRadius: const BorderRadius.all(Radius.circular(25)),
51
- selectedColor: store.state.darkMode
48
+ selectedColor: context.state.darkMode ? Colors.lightBlue.shade300 : Colors.yellowAccent.shade700,
52
- ? Colors.lightBlue.shade300
53
- : Colors.yellowAccent.shade700,
54
49
  selectedBorderColor: Colors.grey,
55
50
  color: Colors.grey,
56
51
  fillColor: Colors.transparent,
@@ -58,7 +53,7 @@ class SettingsSheet extends StatelessWidget {
58
53
  minHeight: 36.0,
59
54
  minWidth: 50.0,
60
55
  ),
61
- isSelected: [!store.state.darkMode, store.state.darkMode],
56
+ isSelected: [!context.state.darkMode, context.state.darkMode],
62
57
  children: const [
63
58
  Icon(Icons.light_mode),
64
59
  Icon(Icons.dark_mode),
@@ -72,7 +67,7 @@ class SettingsSheet extends StatelessWidget {
72
67
  color: context.theme.colorScheme.onSurface,
73
68
  ),
74
69
  trailing: IconButton(
75
- onPressed: () => store.dispatch(UpdateTextScaleAction(0.1)),
70
+ onPressed: () => context.dispatch(UpdateTextScaleAction(0.1)),
76
71
  icon: const Icon(
77
72
  Icons.add_circle_outline,
78
73
  size: 32,
@@ -87,7 +82,7 @@ class SettingsSheet extends StatelessWidget {
87
82
  color: context.theme.colorScheme.onSurface,
88
83
  ),
89
84
  trailing: IconButton(
90
- onPressed: () => store.dispatch(UpdateTextScaleAction(-0.1)),
85
+ onPressed: () => context.dispatch(UpdateTextScaleAction(-0.1)),
91
86
  icon: const Icon(
92
87
  Icons.remove_circle_outline,
93
88
  size: 32,
@@ -102,14 +97,13 @@ class SettingsSheet extends StatelessWidget {
102
97
  color: context.theme.colorScheme.onSurface,
103
98
  ),
104
99
  title: Text(context.l.boldFontTitle),
105
- onToggle: (value) => store.dispatch(ToggleBoldFontAction()),
100
+ onToggle: (value) => context.dispatch(ToggleBoldFontAction()),
106
101
  ),
107
102
  SettingsTile.switchTile(
108
103
  initialValue: context.state.engTitles,
109
- leading:
110
- Icon(Icons.abc, color: context.theme.colorScheme.onSurface),
104
+ leading: Icon(Icons.abc, color: context.theme.colorScheme.onSurface),
111
105
  title: Text(context.l.engTitles),
112
- onToggle: (value) => store.dispatch(ToggleEngTitlesAction()),
106
+ onToggle: (value) => context.dispatch(ToggleEngTitlesAction()),
113
107
  ),
114
108
  ],
115
109
  ),
@@ -137,8 +131,7 @@ class SettingsSheet extends StatelessWidget {
137
131
  onPressed: showTermsAndConditions,
138
132
  ),
139
133
  SettingsTile.navigation(
140
- leading:
141
- const Icon(Icons.share_outlined, color: Colors.blueAccent),
134
+ leading: const Icon(Icons.share_outlined, color: Colors.blueAccent),
142
135
  title: Text(context.l.shareAppTitle),
143
136
  onPressed: shareAppLink,
144
137
  ),
lib/store/actions.dart CHANGED
@@ -1,6 +1,18 @@
1
1
  import "package:async_redux/async_redux.dart";
2
2
  import "package:only_bible_app/models.dart";
3
3
  import "package:only_bible_app/store/app_state.dart";
4
+ 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) {
10
+ return state;
11
+ }
12
+ final bible = await loadBible(state.bibleName);
13
+ return state.copy(bible: bible);
14
+ }
15
+ }
4
16
 
5
17
  class FirstOpenDoneAction extends ReduxAction<AppState> {
6
18
  @override
@@ -68,16 +80,11 @@ class SelectVerseAction extends ReduxAction<AppState> {
68
80
  @override
69
81
  AppState reduce() {
70
82
  final isSelected = state.selectedVerses.any(
71
- (el) =>
72
- el.book == verse.book &&
73
- el.chapter == verse.chapter &&
83
+ (el) => el.book == verse.book && el.chapter == verse.chapter && el.index == verse.index,
74
- el.index == verse.index,
75
84
  );
76
85
  if (isSelected) {
77
86
  return state.copy(
78
- selectedVerses: state.selectedVerses
79
- .where((it) => it.index != verse.index)
87
+ selectedVerses: state.selectedVerses.where((it) => it.index != verse.index).toList(),
80
- .toList(),
81
88
  );
82
89
  } else {
83
90
  return state.copy(
lib/store/app_state.dart CHANGED
@@ -4,7 +4,6 @@ import "package:just_audio/just_audio.dart";
4
4
  import "package:only_bible_app/dialog.dart";
5
5
  import "package:only_bible_app/models.dart";
6
6
  import "package:only_bible_app/store/actions.dart";
7
- import "package:only_bible_app/app.dart";
8
7
  import "package:only_bible_app/store/buffer_audio_source.dart";
9
8
  import "package:only_bible_app/theme.dart";
10
9
  import "package:only_bible_app/utils.dart";
@@ -25,6 +24,7 @@ class AppState {
25
24
  final bool isPlaying;
26
25
  final List<Verse> selectedVerses;
27
26
  final Map<String, int> highlights;
27
+ final Bible? bible;
28
28
 
29
29
  const AppState({
30
30
  this.firstOpen = true,
@@ -39,6 +39,7 @@ class AppState {
39
39
  this.isPlaying = false,
40
40
  this.selectedVerses = const [],
41
41
  this.highlights = const {},
42
+ this.bible,
42
43
  });
43
44
 
44
45
  AppState copy({
@@ -54,6 +55,7 @@ class AppState {
54
55
  bool? isPlaying,
55
56
  List<Verse>? selectedVerses,
56
57
  Map<String, int>? highlights,
58
+ Bible? bible,
57
59
  }) {
58
60
  return AppState(
59
61
  firstOpen: firstOpen ?? this.firstOpen,
@@ -68,6 +70,7 @@ class AppState {
68
70
  isPlaying: isPlaying ?? this.isPlaying,
69
71
  selectedVerses: selectedVerses ?? this.selectedVerses,
70
72
  highlights: highlights ?? this.highlights,
73
+ bible: bible ?? this.bible,
71
74
  );
72
75
  }
73
76
 
@@ -94,9 +97,7 @@ class AppState {
94
97
  textScale: (json["textScale"] as num?)?.toDouble() ?? 0.0,
95
98
  savedBook: json["savedBook"] as int? ?? 0,
96
99
  savedChapter: json["savedChapter"] as int? ?? 0,
97
- highlights: (json["highlights"] as Map<String, dynamic>?)
100
+ highlights: (json["highlights"] as Map<String, dynamic>?)?.map((k, v) => MapEntry(k, v as int)) ?? const {},
98
- ?.map((k, v) => MapEntry(k, v as int)) ??
99
- const {},
100
101
  );
101
102
 
102
103
  Color? getHighlight(Verse v) {
@@ -108,15 +109,13 @@ class AppState {
108
109
 
109
110
  bool isVerseSelected(Verse v) {
110
111
  return selectedVerses.any(
111
- (el) =>
112
- el.book == v.book && el.chapter == v.chapter && el.index == v.index,
112
+ (el) => el.book == v.book && el.chapter == v.chapter && el.index == v.index,
113
113
  );
114
114
  }
115
115
 
116
116
  TextStyle getHighlightStyle(BuildContext context, Verse v, bool heading) {
117
117
  final isSelected = selectedVerses.any(
118
- (el) =>
119
- el.book == v.book && el.chapter == v.chapter && el.index == v.index,
118
+ (el) => el.book == v.book && el.chapter == v.chapter && el.index == v.index,
120
119
  );
121
120
 
122
121
  if (!heading && isSelected) {
@@ -128,9 +127,7 @@ class AppState {
128
127
  if (darkMode) {
129
128
  return TextStyle(
130
129
  backgroundColor: highlight?.withValues(alpha: 0.7),
131
- color: highlight != null
132
- ? Colors.white
133
- : Theme.of(context).colorScheme.onSurface,
130
+ color: highlight != null ? Colors.white : Theme.of(context).colorScheme.onSurface,
134
131
  );
135
132
  }
136
133
  return TextStyle(
@@ -139,17 +136,16 @@ class AppState {
139
136
  }
140
137
 
141
138
  Future<void> onPlay(BuildContext context, Bible bible) async {
142
- final versesToPlay = List<Verse>.from(store.state.selectedVerses);
139
+ final versesToPlay = List<Verse>.from(context.state.selectedVerses);
143
- if (store.state.isPlaying) {
140
+ if (context.state.isPlaying) {
144
141
  await player.pause();
145
- store.dispatch(SetPlayingAction(false));
142
+ context.dispatch(SetPlayingAction(false));
146
143
  } else {
147
- store.dispatch(SetPlayingAction(true));
144
+ context.dispatch(SetPlayingAction(true));
148
145
  for (final v in versesToPlay) {
149
146
  final pathname = "${bible.name}_${v.book}_${v.chapter}_${v.index}";
150
147
  try {
151
- final data =
152
- await convertText(context.currentLang.audioVoice, v.text);
148
+ final data = await convertText(context.currentLang.audioVoice, v.text);
153
149
  await player.setAudioSource(BufferAudioSource(data));
154
150
  await player.play();
155
151
  await player.stop();
@@ -166,7 +162,7 @@ class AppState {
166
162
  return;
167
163
  } finally {
168
164
  await player.pause();
169
- store.dispatch(SetPlayingAction(false));
165
+ context.dispatch(SetPlayingAction(false));
170
166
  }
171
167
  }
172
168
  }
lib/theme.dart CHANGED
@@ -162,7 +162,7 @@ final lightTheme = ThemeData(
162
162
  ),
163
163
  headlineMedium: TextStyle(
164
164
  color: lightColorScheme.onSurface,
165
- fontSize: 19,
165
+ fontSize: 24,
166
166
  fontWeight: FontWeight.w500,
167
167
  letterSpacing: 0.5,
168
168
  ),
@@ -172,6 +172,12 @@ final lightTheme = ThemeData(
172
172
  color: lightColorScheme.primary,
173
173
  letterSpacing: 0,
174
174
  ),
175
+ labelLarge: TextStyle(
176
+ fontSize: 16,
177
+ fontWeight: FontWeight.w700,
178
+ color: lightColorScheme.onSurface,
179
+ letterSpacing: 0,
180
+ ),
175
181
  ),
176
182
  );
177
183
 
lib/utils.dart CHANGED
@@ -2,22 +2,19 @@ import "dart:convert";
2
2
  import "package:async_redux/async_redux.dart";
3
3
  import "package:http/http.dart" as http;
4
4
  import "package:flutter/services.dart";
5
- import "package:only_bible_app/dialog.dart";
6
- import "package:only_bible_app/env.dart";
7
- import "package:only_bible_app/models.dart";
8
- import "package:only_bible_app/store/app_state.dart";
9
-
10
5
  import "package:package_info_plus/package_info_plus.dart";
11
6
  import "package:url_launcher/url_launcher.dart";
12
- import "package:flutter/foundation.dart"
7
+ import "package:flutter/foundation.dart" show TargetPlatform, defaultTargetPlatform, kDebugMode;
13
- show TargetPlatform, defaultTargetPlatform, kDebugMode;
14
8
  import "package:flutter/material.dart";
15
9
  import "package:only_bible_app/l10n/app_localizations.dart";
16
10
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
11
+ import "package:only_bible_app/dialog.dart";
12
+ import "package:only_bible_app/env.dart";
13
+ import "package:only_bible_app/models.dart";
14
+ import "package:only_bible_app/store/app_state.dart";
17
15
 
18
16
  extension IterableUtils<E> on Iterable<E> {
19
- Iterable<E> sortedBy(Comparable Function(E e) key) =>
17
+ Iterable<E> sortedBy(Comparable Function(E e) key) => toList()..sort((a, b) => key(a).compareTo(key(b)));
20
- toList()..sort((a, b) => key(a).compareTo(key(b)));
21
18
 
22
19
  Iterable<E> removeBy(bool Function(E e) key) => toList()..removeWhere(key);
23
20
 
@@ -45,12 +42,15 @@ extension AppContext on BuildContext {
45
42
 
46
43
  AppState get state => StoreProvider.state<AppState>(this);
47
44
 
45
+ void dispatch(ReduxAction<AppState> action) => StoreProvider.dispatch<AppState>(this, action);
46
+
47
+ Future<void> dispatchAndWait(ReduxAction<AppState> action) => StoreProvider.dispatchAndWait<AppState>(this, action);
48
+
48
49
  AppLocalizations get l => state.engTitles && state.languageCode != "en"
49
50
  ? lookupAppLocalizations(const Locale("en"))
50
51
  : AppLocalizations.of(this)!;
51
52
 
52
- AppLocalizations get currentLang => supportedLocalizations
53
- .firstWhere((el) => el.languageCode == state.languageCode);
53
+ AppLocalizations get currentLang => supportedLocalizations.firstWhere((el) => el.languageCode == state.languageCode);
54
54
 
55
55
  double get actionsHeight {
56
56
  if (isIOS()) {
@@ -183,8 +183,7 @@ Future<Uint8List> convertText(String langCode, String text) async {
183
183
 
184
184
  Future<Bible> loadBible(String name) async {
185
185
  final bytes = await rootBundle.load("assets/bibles/$name.txt");
186
- final books = parseBible(
187
- name, utf8.decode(bytes.buffer.asUint8List(), allowMalformed: false));
186
+ final books = parseBible(name, utf8.decode(bytes.buffer.asUint8List(), allowMalformed: false));
188
187
  // await Future.delayed(Duration(seconds: 2));
189
188
  return Bible(
190
189
  name: name,
lib/widgets/sliver_heading.dart CHANGED
@@ -25,8 +25,10 @@ class SliverHeading extends StatelessWidget {
25
25
  crossAxisAlignment: CrossAxisAlignment.center,
26
26
  children: [
27
27
  Expanded(
28
- child: Text(title,
28
+ child: Text(
29
+ title,
29
- style: Theme.of(context).textTheme.headlineMedium),
30
+ style: Theme.of(context).textTheme.headlineMedium,
31
+ ),
30
32
  ),
31
33
  if (showClose)
32
34
  IconButton(
lib/widgets/verses_view.dart CHANGED
@@ -3,7 +3,6 @@ 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
5
  import "package:only_bible_app/navigation.dart";
6
- import "package:only_bible_app/app.dart";
7
6
  import "package:only_bible_app/store/actions.dart";
8
7
  import "package:only_bible_app/utils.dart";
9
8
 
@@ -55,40 +54,28 @@ class VersesView extends StatelessWidget {
55
54
  (v) => [
56
55
  if (v.heading != "")
57
56
  TextSpan(
58
- text:
59
- "${v.heading.replaceAll("<br>", "\n")}\n",
57
+ text: "${v.heading.replaceAll("<br>", "\n")}\n",
60
- style: context.state
58
+ style: Theme.of(context).textTheme.labelLarge,
61
- .getHighlightStyle(context, v, true)
62
- .copyWith(
63
- fontSize: 18,
64
- fontWeight: FontWeight.w600,
65
- height: 2,
66
- ),
67
59
  ),
68
60
  WidgetSpan(
69
61
  child: Transform.translate(
70
62
  offset: const Offset(0, -2),
71
63
  child: Text(
72
64
  "${v.index + 1} ",
73
- style: Theme.of(context)
65
+ style: Theme.of(context).textTheme.labelMedium,
74
- .textTheme
75
- .labelMedium,
76
66
  ),
77
67
  ),
78
68
  ),
79
69
  TextSpan(
80
70
  text: "${v.text}\n",
81
- style: context.state
82
- .getHighlightStyle(context, v, false),
71
+ style: context.state.getHighlightStyle(context, v, false),
83
72
  recognizer: TapGestureRecognizer()
84
73
  ..onTap = () {
85
- store.dispatch(SelectVerseAction(v));
74
+ context.dispatch(SelectVerseAction(v));
86
- if (store
87
- .state.selectedVerses.isNotEmpty) {
75
+ if (context.state.selectedVerses.isNotEmpty) {
88
76
  showActions(context, bible);
89
77
  }
90
- if (store
91
- .state.selectedVerses.isEmpty) {
78
+ if (context.state.selectedVerses.isEmpty) {
92
79
  hideActions(context);
93
80
  }
94
81
  },
@@ -110,8 +97,7 @@ class VersesView extends StatelessWidget {
110
97
  ),
111
98
  Padding(
112
99
  padding: EdgeInsets.only(
113
- bottom:
114
- context.state.selectedVerses.isNotEmpty ? 120 : 0,
100
+ bottom: context.state.selectedVerses.isNotEmpty ? 120 : 0,
115
101
  ),
116
102
  ),
117
103
  ],