~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 only-bible-app
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
@@ -76,7 +76,7 @@ class App extends StatelessWidget {
76
76
  child: Builder(
77
77
  builder: (context) => MaterialApp.router(
78
78
  routerConfig: _router,
79
- title: "Bible",
79
+ title: "Only Bible App",
80
80
  debugShowCheckedModeBanner: false,
81
81
  themeMode: context.select((s) => s.darkMode) ? ThemeMode.dark : ThemeMode.light,
82
82
  theme: lightTheme,
lib/main.dart CHANGED
@@ -3,7 +3,6 @@ import "package:flutter/scheduler.dart";
3
3
  import "package:flutter/services.dart";
4
4
  import "package:flutter/foundation.dart";
5
5
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
6
- import "package:flutter_soloud/flutter_soloud.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";
@@ -72,7 +71,6 @@ void main() async {
72
71
  }
73
72
  return true;
74
73
  };
75
- await SoLoud.instance.init();
76
74
  FlutterAzureTts.init(
77
75
  subscriptionKey: Env.ttsSubscriptionKey,
78
76
  region: "centralindia",
@@ -82,7 +80,8 @@ void main() async {
82
80
  final json = await persistor.readJson();
83
81
  final bibleName = json?["bibleName"] as String? ?? "en_kjv";
84
82
  final bible = await loadBible(bibleName);
83
+ final initialState =
85
- final initialState = json != null ? AppState.fromJson(json, bible) : AppState(bible: bible);
84
+ json != null ? AppState.fromJson(json, bible) : AppState(bible: bible);
86
85
  final store = Store<AppState>(
87
86
  initialState: initialState,
88
87
  persistor: persistor,
lib/store/actions_navigation.dart CHANGED
@@ -1,6 +1,5 @@
1
1
  import "package:async_redux/async_redux.dart";
2
2
  import "package:flutter/material.dart";
3
- import "package:flutter_soloud/flutter_soloud.dart";
4
3
  import "package:go_router/go_router.dart";
5
4
  import "package:only_bible_app/gen/bible.gen.dart";
6
5
  import "package:only_bible_app/store/app_state.dart";
@@ -119,23 +118,37 @@ class GoToChapterAction extends ReduxAction<AppState> {
119
118
  GoToChapterAction(this.router, this.book, this.chapter, {this.verse});
120
119
 
121
120
  @override
122
- AppState reduce() {
121
+ Future<AppState> reduce() async {
123
- SoLoud.instance.disposeAllSources();
122
+ await stopAudioPlayback();
124
123
  final navId = DateTime.now().millisecondsSinceEpoch;
124
+ if (book > state.savedBook ||
125
- if (book > state.savedBook || (book == state.savedBook && chapter > state.savedChapter)) {
125
+ (book == state.savedBook && chapter > state.savedChapter)) {
126
126
  router.push(
127
127
  "/chapter/$book/$chapter",
128
+ extra: (
128
- extra: (slideDir: TextDirection.ltr, scrollToVerse: verse, navId: navId),
129
+ slideDir: TextDirection.ltr,
130
+ scrollToVerse: verse,
131
+ navId: navId
132
+ ),
129
133
  );
134
+ } else if (book < state.savedBook ||
130
- } else if (book < state.savedBook || (book == state.savedBook && chapter < state.savedChapter)) {
135
+ (book == state.savedBook && chapter < state.savedChapter)) {
131
136
  router.push(
132
137
  "/chapter/$book/$chapter",
138
+ extra: (
133
- extra: (slideDir: TextDirection.rtl, scrollToVerse: verse, navId: navId),
139
+ slideDir: TextDirection.rtl,
140
+ scrollToVerse: verse,
141
+ navId: navId
142
+ ),
134
143
  );
135
144
  } else {
136
145
  router.push(
137
146
  "/chapter/$book/$chapter",
147
+ extra: (
138
- extra: (slideDir: null as TextDirection?, scrollToVerse: verse, navId: navId),
148
+ slideDir: null as TextDirection?,
149
+ scrollToVerse: verse,
150
+ navId: navId
151
+ ),
139
152
  );
140
153
  }
141
154
  return state.copy(
@@ -172,7 +185,7 @@ class NextChapterAction extends ReduxAction<AppState> {
172
185
  NextChapterAction(this.router, this.bible, this.book, this.chapter);
173
186
 
174
187
  @override
175
- AppState? reduce() {
188
+ Future<AppState?> reduce() async {
176
189
  final selectedBook = bible.books![book];
177
190
  int? newBook;
178
191
  int? newChapter;
@@ -187,10 +200,14 @@ class NextChapterAction extends ReduxAction<AppState> {
187
200
  }
188
201
 
189
202
  if (newBook == null) return null;
190
- SoLoud.instance.disposeAllSources();
203
+ await stopAudioPlayback();
191
204
  router.pushReplacement(
192
205
  "/chapter/$newBook/$newChapter",
206
+ extra: (
207
+ slideDir: TextDirection.ltr,
208
+ scrollToVerse: null as int?,
193
- extra: (slideDir: TextDirection.ltr, scrollToVerse: null as int?, navId: DateTime.now().millisecondsSinceEpoch),
209
+ navId: DateTime.now().millisecondsSinceEpoch
210
+ ),
194
211
  );
195
212
  return state.copy(
196
213
  savedBook: newBook,
@@ -209,7 +226,7 @@ class PreviousChapterAction extends ReduxAction<AppState> {
209
226
  PreviousChapterAction(this.router, this.bible, this.book, this.chapter);
210
227
 
211
228
  @override
212
- AppState? reduce() {
229
+ Future<AppState?> reduce() async {
213
230
  final selectedBook = bible.books![book];
214
231
  int? newBook;
215
232
  int? newChapter;
@@ -224,10 +241,14 @@ class PreviousChapterAction extends ReduxAction<AppState> {
224
241
  }
225
242
 
226
243
  if (newBook == null) return null;
227
- SoLoud.instance.disposeAllSources();
244
+ await stopAudioPlayback();
228
245
  router.pushReplacement(
229
246
  "/chapter/$newBook/$newChapter",
247
+ extra: (
248
+ slideDir: TextDirection.rtl,
249
+ scrollToVerse: null as int?,
230
- extra: (slideDir: TextDirection.rtl, scrollToVerse: null as int?, navId: DateTime.now().millisecondsSinceEpoch),
250
+ navId: DateTime.now().millisecondsSinceEpoch
251
+ ),
231
252
  );
232
253
  return state.copy(
233
254
  savedBook: newBook,
@@ -266,8 +287,10 @@ class ShareVersesAction extends ReduxAction<AppState> {
266
287
  Future<AppState?> reduce() async {
267
288
  final chapter = verses.first.chapter + 1;
268
289
  final items = verses.sortedBy((e) => e.index).map((e) => e.index + 1);
290
+ final versesThrough =
269
- final versesThrough = items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
291
+ items.length >= 3 ? "${items.first}-${items.last}" : items.join(",");
292
+ final version =
270
- final version = state.bible.languageCode == "en" ? state.bible.languageNative : "";
293
+ state.bible.languageCode == "en" ? state.bible.languageNative : "";
271
294
  final title = "$bookName $chapter:$versesThrough $version";
272
295
  final text = verses.map((e) => _stripRedTags(e.text ?? "")).join("\n");
273
296
  await SharePlus.instance.share(
lib/store/actions_state.dart CHANGED
@@ -54,8 +54,8 @@ class TogglePlayAction extends ReduxAction<AppState> {
54
54
 
55
55
  @override
56
56
  Future<AppState?> reduce() async {
57
- if (SoLoud.instance.getActiveVoiceCount() > 0) {
57
+ if (isAudioPlaybackActive()) {
58
- await SoLoud.instance.disposeAllSources();
58
+ await stopAudioPlayback();
59
59
  return null;
60
60
  }
61
61
  final versesToPlay = List<Verse>.from(state.selectedVerses);
@@ -65,6 +65,7 @@ class TogglePlayAction extends ReduxAction<AppState> {
65
65
  final pathname = "${bible.name!}_${v.book}_${v.chapter}_${v.index}";
66
66
  try {
67
67
  final data = await convertText(audioVoice, v.text ?? "");
68
+ await ensureAudioPlaybackInitialized();
68
69
  final stream = SoLoud.instance.setBufferStream(
69
70
  bufferingType: BufferingType.preserved,
70
71
  format: BufferType.auto,
@@ -74,12 +75,14 @@ class TogglePlayAction extends ReduxAction<AppState> {
74
75
  final handle = await SoLoud.instance.play(stream);
75
76
  // Wait for playback to complete
76
77
  await stream.soundEvents.firstWhere(
78
+ (e) =>
77
- (e) => e.event == SoundEventType.handleIsNoMoreValid && e.handle == handle,
79
+ (e.event == SoundEventType.handleIsNoMoreValid &&
80
+ e.handle == handle) ||
81
+ e.event == SoundEventType.soundDisposed,
78
82
  );
79
- await SoLoud.instance.disposeSource(stream);
80
83
  } catch (err) {
81
84
  // If sources were disposed (user stopped playback), don't treat as error
82
- if (SoLoud.instance.getActiveVoiceCount() == 0) {
85
+ if (!SoLoud.instance.isInitialized || !isAudioPlaybackActive()) {
83
86
  return null;
84
87
  }
85
88
  log(
@@ -92,7 +95,9 @@ class TogglePlayAction extends ReduxAction<AppState> {
92
95
  showError(buildContext, audioError);
93
96
  }
94
97
  return null;
95
- } finally {}
98
+ } finally {
99
+ await stopAudioPlayback();
100
+ }
96
101
  }
97
102
  return null;
98
103
  }
@@ -106,7 +111,10 @@ class SelectVerseAction extends ReduxAction<AppState> {
106
111
  @override
107
112
  AppState reduce() {
108
113
  final isSelected = state.selectedVerses.any(
114
+ (el) =>
115
+ el.book == verse.book &&
109
- (el) => el.book == verse.book && el.chapter == verse.chapter && el.index == verse.index,
116
+ el.chapter == verse.chapter &&
117
+ el.index == verse.index,
110
118
  );
111
119
  final newVerses = isSelected
112
120
  ? state.selectedVerses.where((it) => it.index != verse.index).toList()
lib/utils.dart CHANGED
@@ -4,9 +4,11 @@ import "package:archive/archive.dart";
4
4
  import "package:http/http.dart" as http;
5
5
  import "package:flutter/services.dart";
6
6
  import "package:package_info_plus/package_info_plus.dart";
7
- import "package:flutter/foundation.dart" show TargetPlatform, defaultTargetPlatform, kDebugMode;
7
+ import "package:flutter/foundation.dart"
8
+ show TargetPlatform, defaultTargetPlatform, kDebugMode;
8
9
  import "package:flutter/material.dart";
9
10
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
11
+ import "package:flutter_soloud/flutter_soloud.dart";
10
12
  import "package:only_bible_app/env.dart";
11
13
  import "package:only_bible_app/gen/bible.gen.dart";
12
14
  export "package:only_bible_app/bible_data.dart";
@@ -31,7 +33,8 @@ extension BookExt on Book {
31
33
  }
32
34
 
33
35
  extension IterableUtils<E> on Iterable<E> {
34
- Iterable<E> sortedBy(Comparable Function(E e) key) => toList()..sort((a, b) => key(a).compareTo(key(b)));
36
+ Iterable<E> sortedBy(Comparable Function(E e) key) =>
37
+ toList()..sort((a, b) => key(a).compareTo(key(b)));
35
38
 
36
39
  Iterable<E> removeBy(bool Function(E e) key) => toList()..removeWhere(key);
37
40
 
@@ -74,9 +77,32 @@ Future<Uint8List> convertText(String langCode, String text) async {
74
77
  return ttsResponse.audio.buffer.asUint8List();
75
78
  }
76
79
 
80
+ bool isAudioPlaybackActive() {
81
+ return SoLoud.instance.isInitialized &&
82
+ SoLoud.instance.getActiveVoiceCount() > 0;
83
+ }
84
+
85
+ Future<void> ensureAudioPlaybackInitialized() async {
86
+ if (!SoLoud.instance.isInitialized) {
87
+ await SoLoud.instance.init();
88
+ }
89
+ }
90
+
91
+ Future<void> stopAudioPlayback() async {
92
+ if (!SoLoud.instance.isInitialized) return;
93
+ try {
94
+ await SoLoud.instance.disposeAllSources();
95
+ } finally {
96
+ if (SoLoud.instance.isInitialized) {
97
+ SoLoud.instance.deinit();
98
+ }
99
+ }
100
+ }
101
+
77
102
  Future<Bible> loadBible(String name) async {
78
103
  final data = await rootBundle.load("assets/bibles/$name.bin.gz");
104
+ final decompressed =
79
- final decompressed = const GZipDecoder().decodeBytes(data.buffer.asUint8List());
105
+ const GZipDecoder().decodeBytes(data.buffer.asUint8List());
80
106
  return Bible(decompressed);
81
107
  }
82
108
 
web/index.html CHANGED
@@ -21,13 +21,13 @@
21
21
  <!-- iOS meta tags & icons -->
22
22
  <meta name="mobile-web-app-capable" content="yes">
23
23
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
24
- <meta name="apple-mobile-web-app-title" content="web">
24
+ <meta name="apple-mobile-web-app-title" content="Only Bible App">
25
25
  <link rel="apple-touch-icon" href="icons/Icon-192.png">
26
26
 
27
27
  <!-- Favicon -->
28
28
  <link rel="icon" type="image/png" href="favicon.png">
29
29
 
30
- <title>web</title>
30
+ <title>Only Bible App</title>
31
31
  <link rel="manifest" href="manifest.json">
32
32
 
33
33