only-bible-app v3.7.2+24

#kotlin#android#ios

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

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


c2e460a — Peter John 2026-08-08T13:34:21+05:30
fix navigatino

Generated files not shown: pubspec.lock

.gitignore CHANGED
@@ -17,6 +17,7 @@ node_modules
17
17
  *.aab
18
18
  *.apk
19
19
  *.ipa
20
+ *.dSYM.zip
20
21
 
21
22
  # Python
22
23
  .venv
ios/Runner.xcodeproj/project.pbxproj CHANGED
@@ -485,7 +485,7 @@
485
485
  CLANG_ENABLE_MODULES = YES;
486
486
  CODE_SIGN_IDENTITY = "Apple Development";
487
487
  CODE_SIGN_STYLE = Automatic;
488
- CURRENT_PROJECT_VERSION = 4;
488
+ CURRENT_PROJECT_VERSION = 5;
489
489
  DEVELOPMENT_TEAM = FXH8NUGFYY;
490
490
  ENABLE_BITCODE = NO;
491
491
  INFOPLIST_FILE = Runner/Info.plist;
@@ -508,7 +508,7 @@
508
508
  buildSettings = {
509
509
  BUNDLE_LOADER = "$(TEST_HOST)";
510
510
  CODE_SIGN_STYLE = Automatic;
511
- CURRENT_PROJECT_VERSION = 4;
511
+ CURRENT_PROJECT_VERSION = 5;
512
512
  GENERATE_INFOPLIST_FILE = YES;
513
513
  MARKETING_VERSION = 1.0;
514
514
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -526,7 +526,7 @@
526
526
  buildSettings = {
527
527
  BUNDLE_LOADER = "$(TEST_HOST)";
528
528
  CODE_SIGN_STYLE = Automatic;
529
- CURRENT_PROJECT_VERSION = 4;
529
+ CURRENT_PROJECT_VERSION = 5;
530
530
  GENERATE_INFOPLIST_FILE = YES;
531
531
  MARKETING_VERSION = 1.0;
532
532
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -542,7 +542,7 @@
542
542
  buildSettings = {
543
543
  BUNDLE_LOADER = "$(TEST_HOST)";
544
544
  CODE_SIGN_STYLE = Automatic;
545
- CURRENT_PROJECT_VERSION = 4;
545
+ CURRENT_PROJECT_VERSION = 5;
546
546
  GENERATE_INFOPLIST_FILE = YES;
547
547
  MARKETING_VERSION = 1.0;
548
548
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -671,7 +671,7 @@
671
671
  CLANG_ENABLE_MODULES = YES;
672
672
  CODE_SIGN_IDENTITY = "Apple Development";
673
673
  CODE_SIGN_STYLE = Automatic;
674
- CURRENT_PROJECT_VERSION = 4;
674
+ CURRENT_PROJECT_VERSION = 5;
675
675
  DEVELOPMENT_TEAM = FXH8NUGFYY;
676
676
  ENABLE_BITCODE = NO;
677
677
  INFOPLIST_FILE = Runner/Info.plist;
@@ -698,7 +698,7 @@
698
698
  CODE_SIGN_IDENTITY = "Apple Development";
699
699
  "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
700
700
  CODE_SIGN_STYLE = Manual;
701
- CURRENT_PROJECT_VERSION = 4;
701
+ CURRENT_PROJECT_VERSION = 5;
702
702
  DEVELOPMENT_TEAM = "";
703
703
  "DEVELOPMENT_TEAM[sdk=iphoneos*]" = FXH8NUGFYY;
704
704
  ENABLE_BITCODE = NO;
ios/Runner/Info.plist CHANGED
@@ -23,7 +23,7 @@
23
23
  <key>CFBundleSignature</key>
24
24
  <string>????</string>
25
25
  <key>CFBundleVersion</key>
26
- <string>4</string>
26
+ <string>5</string>
27
27
  <key>LSRequiresIPhoneOS</key>
28
28
  <true/>
29
29
  <key>UIApplicationSceneManifest</key>
@@ -72,10 +72,6 @@
72
72
  <false/>
73
73
  <key>NSPhotoLibraryUsageDescription</key>
74
74
  <string>Only Bible App uses the system file picker to let you choose a backup file to restore your highlights and settings. Depending on your device, this picker may show your photo library, though Only Bible App does not access your photos directly.</string>
75
- <key>NSCameraUsageDescription</key>
76
- <string>Only Bible App does not use the camera. This description is required because a bundled library references camera APIs, even though this app does not use them.</string>
77
- <key>NSLocationWhenInUseUsageDescription</key>
78
- <string>Only Bible App does not use your location. This description is required because a bundled library references location APIs, even though this app does not use them.</string>
79
75
  <key>NSAppleMusicUsageDescription</key>
80
76
  <string>Only Bible App does not access your music library. This description is required because a bundled library references media library APIs, even though this app does not use them.</string>
81
77
  </dict>
lib/app.dart CHANGED
@@ -22,31 +22,53 @@ class App extends StatelessWidget {
22
22
  path: "/chapter/:bookIndex/:chapterIndex",
23
23
  pageBuilder: (context, state) {
24
24
  final bookIndex = int.parse(state.pathParameters["bookIndex"]!);
25
+ final chapterIndex = int.parse(
25
- final chapterIndex = int.parse(state.pathParameters["chapterIndex"]!);
26
+ state.pathParameters["chapterIndex"]!,
27
+ );
28
+ final extra =
29
+ state.extra
30
+ as ({
26
- final extra = state.extra as ({TextDirection? slideDir, int? scrollToVerse, int navId})?;
31
+ TextDirection? slideDir,
32
+ int? scrollToVerse,
33
+ int navId,
34
+ })?;
27
35
  final slideDir = extra?.slideDir;
28
36
  final scrollToVerse = extra?.scrollToVerse;
37
+ final pageKey = ValueKey(
29
- final pageKey = ValueKey("$bookIndex-$chapterIndex-${extra?.navId ?? 0}");
38
+ "$bookIndex-$chapterIndex-${extra?.navId ?? 0}",
39
+ );
30
40
  if (slideDir != null) {
31
41
  return CustomTransitionPage(
32
42
  key: pageKey,
43
+ child: Home(
44
+ bookIndex: bookIndex,
45
+ chapterIndex: chapterIndex,
33
- child: Home(bookIndex: bookIndex, chapterIndex: chapterIndex, scrollToVerse: scrollToVerse),
46
+ scrollToVerse: scrollToVerse,
47
+ ),
48
+ transitionsBuilder:
34
- transitionsBuilder: (context, animation, secondaryAnimation, child) {
49
+ (context, animation, secondaryAnimation, child) {
35
- const begin = Offset(1.0, 0.0);
50
+ const begin = Offset(1.0, 0.0);
36
- const end = Offset.zero;
51
+ const end = Offset.zero;
37
- const curve = Curves.ease;
52
+ const curve = Curves.ease;
53
+ final tween = Tween(
54
+ begin: begin,
55
+ end: end,
38
- final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
56
+ ).chain(CurveTween(curve: curve));
39
- return SlideTransition(
57
+ return SlideTransition(
40
- textDirection: slideDir,
58
+ textDirection: slideDir,
41
- position: animation.drive(tween),
59
+ position: animation.drive(tween),
42
- child: child,
60
+ child: child,
43
- );
61
+ );
44
- },
62
+ },
45
63
  );
46
64
  }
47
65
  return NoTransitionPage(
48
66
  key: pageKey,
67
+ child: Home(
68
+ bookIndex: bookIndex,
69
+ chapterIndex: chapterIndex,
49
- child: Home(bookIndex: bookIndex, chapterIndex: chapterIndex, scrollToVerse: scrollToVerse),
70
+ scrollToVerse: scrollToVerse,
71
+ ),
50
72
  );
51
73
  },
52
74
  ),
@@ -57,7 +79,8 @@ class App extends StatelessWidget {
57
79
  }
58
80
 
59
81
  void _syncCurrentChapter() {
82
+ final segments =
60
- final segments = _router.routerDelegate.currentConfiguration.uri.pathSegments;
83
+ _router.routerDelegate.currentConfiguration.uri.pathSegments;
61
84
  if (segments.length != 3 || segments.first != "chapter") return;
62
85
 
63
86
  final book = int.tryParse(segments[1]);
lib/dialog.dart CHANGED
@@ -4,6 +4,7 @@ import "package:flutter/material.dart";
4
4
  import "package:only_bible_app/bible_data.dart";
5
5
  import "package:only_bible_app/gen/bible.gen.dart";
6
6
  import "package:only_bible_app/store/actions_navigation.dart";
7
+ import "package:only_bible_app/store/app_navigator.dart";
7
8
  import "package:only_bible_app/store/app_state.dart";
8
9
  export "package:shared/shared.dart" show showAlert, showError, showConfirm;
9
10
 
@@ -14,7 +15,10 @@ void showBibleSelectDialog(BuildContext context, Bible bible) {
14
15
  builder: (dialogContext) {
15
16
  return AlertDialog(
16
17
  title: Text(bible.bibleSelectTitle!),
17
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
18
+ contentPadding: const EdgeInsets.symmetric(
19
+ horizontal: 16,
20
+ vertical: 12,
21
+ ),
18
22
  content: SizedBox(
19
23
  // Capped so the list doesn't stretch edge-to-edge on wide
20
24
  // desktop/tablet windows, where double.maxFinite would otherwise
@@ -23,7 +27,7 @@ void showBibleSelectDialog(BuildContext context, Bible bible) {
23
27
  height: 400,
24
28
  child: ListView.separated(
25
29
  itemCount: bibleItems.length,
26
- separatorBuilder: (_, __) => const Divider(height: 1),
30
+ separatorBuilder: (_, _) => const Divider(height: 1),
27
31
  itemBuilder: (itemContext, index) {
28
32
  final item = bibleItems.values.elementAt(index);
29
33
  final isSelected = context.read().bible.name == item.fileName;
@@ -31,16 +35,31 @@ void showBibleSelectDialog(BuildContext context, Bible bible) {
31
35
  selected: isSelected,
32
36
  tileColor: null,
33
37
  leading: const Icon(Icons.menu_book),
38
+ title: Text(
39
+ item.languageNative,
34
- title: Text(item.languageNative, style: const TextStyle(fontWeight: FontWeight.w600)),
40
+ style: const TextStyle(fontWeight: FontWeight.w600),
41
+ ),
35
42
  subtitle: (item.languageNative != item.languageEnglish)
43
+ ? Text(
44
+ item.languageEnglish,
45
+ style: const TextStyle(
46
+ fontSize: 15,
36
- ? Text(item.languageEnglish, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400))
47
+ fontWeight: FontWeight.w400,
48
+ ),
49
+ )
37
50
  : null,
38
51
  trailing: isSelected ? const Icon(Icons.check) : null,
39
52
  onTap: () {
40
53
  final s = context.read();
54
+ final router = context.router;
41
55
  Navigator.of(itemContext).pop();
42
56
  context.dispatch(
43
- UpdateCurrentBibleAction(item.fileName, s.savedBook, s.savedChapter),
57
+ UpdateCurrentBibleAction(
58
+ router,
59
+ item.fileName,
60
+ s.savedBook,
61
+ s.savedChapter,
62
+ ),
44
63
  );
45
64
  },
46
65
  );
lib/home.dart CHANGED
@@ -132,7 +132,7 @@ class _HomeState extends State<Home> {
132
132
  onEdgeSwipeRight: () {
133
133
  final router = context.router;
134
134
  if (router.canPop()) {
135
- router.pop();
135
+ context.dispatch(PopChapterAction(router));
136
136
  } else {
137
137
  context.dispatch(PreviousChapterAction(router, bible, chapter.book, chapter.index));
138
138
  }
lib/store/actions_navigation.dart CHANGED
@@ -131,6 +131,12 @@ class ShowChapterSelectAction extends ReduxAction<AppState> {
131
131
  }
132
132
  }
133
133
 
134
+ // Tracks the (book, chapter) each GoToChapterAction push navigated away
135
+ // from, so PopChapterAction can restore savedBook/savedChapter synchronously
136
+ // instead of waiting on the routerDelegate listener, which does not fire
137
+ // until a pop's page transition animation completes (see PopChapterAction).
138
+ final List<(int, int)> _chapterHistory = [];
139
+
134
140
  class GoToChapterAction extends ReduxAction<AppState> {
135
141
  final GoRouter router;
136
142
  final int book;
@@ -153,6 +159,7 @@ class GoToChapterAction extends ReduxAction<AppState> {
153
159
  } else {
154
160
  slideDir = null;
155
161
  }
162
+ _chapterHistory.add((state.savedBook, state.savedChapter));
156
163
  // Update savedBook/savedChapter before pushing: GoRouter notifies the
157
164
  // routerDelegate listener (App._syncCurrentChapter) synchronously as
158
165
  // part of push(), which dispatches SyncCurrentChapterAction. Setting
@@ -167,6 +174,27 @@ class GoToChapterAction extends ReduxAction<AppState> {
167
174
  }
168
175
  }
169
176
 
177
+ class PopChapterAction extends ReduxAction<AppState> {
178
+ final GoRouter router;
179
+
180
+ PopChapterAction(this.router);
181
+
182
+ @override
183
+ AppState? reduce() {
184
+ // Unlike push(), GoRouterDelegate.pop() only notifies its listener once
185
+ // the page's exit transition finishes, so App._syncCurrentChapter can't
186
+ // be relied on to update savedBook/savedChapter promptly. Restore them
187
+ // from our own history here, before popping, the same way GoToChapterAction
188
+ // updates them before pushing.
189
+ if (_chapterHistory.isNotEmpty) {
190
+ final (book, chapter) = _chapterHistory.removeLast();
191
+ dispatch(UpdateChapterAction(book, chapter));
192
+ }
193
+ router.pop();
194
+ return null;
195
+ }
196
+ }
197
+
170
198
  class SyncCurrentChapterAction extends ReduxAction<AppState> {
171
199
  final int book;
172
200
  final int chapter;
@@ -245,16 +273,22 @@ class PreviousChapterAction extends ReduxAction<AppState> {
245
273
  }
246
274
 
247
275
  class UpdateCurrentBibleAction extends ReduxAction<AppState> {
276
+ final GoRouter router;
248
277
  final String name;
249
278
  final int book;
250
279
  final int chapter;
251
280
 
252
- UpdateCurrentBibleAction(this.name, this.book, this.chapter);
281
+ UpdateCurrentBibleAction(this.router, this.name, this.book, this.chapter);
253
282
 
254
283
  @override
255
284
  Future<AppState> reduce() async {
256
285
  final bible = await loadBible(name);
257
286
  final (newBook, newChapter) = clampBookChapter(bible, book, chapter);
287
+ // Keep the route in sync with the clamped book/chapter: otherwise the
288
+ // URL keeps pointing at the pre-switch (possibly now out-of-range)
289
+ // chapter, and a later App._syncCurrentChapter dispatch would clobber
290
+ // savedBook/savedChapter back to those stale path segments.
291
+ router.pushReplacement("/chapter/$newBook/$newChapter");
258
292
  return state.copy(
259
293
  bible: bible,
260
294
  savedBook: newBook,
macos/Runner/Info.plist CHANGED
@@ -34,10 +34,6 @@
34
34
  <string>NSApplication</string>
35
35
  <key>NSPhotoLibraryUsageDescription</key>
36
36
  <string>Only Bible App uses the system file picker to let you choose a backup file to restore your highlights and settings. Depending on your device, this picker may show your photo library, though Only Bible App does not access your photos directly.</string>
37
- <key>NSCameraUsageDescription</key>
38
- <string>Only Bible App does not use the camera. This description is required because a bundled library references camera APIs, even though this app does not use them.</string>
39
- <key>NSLocationWhenInUseUsageDescription</key>
40
- <string>Only Bible App does not use your location. This description is required because a bundled library references location APIs, even though this app does not use them.</string>
41
37
  <key>NSAppleMusicUsageDescription</key>
42
38
  <string>Only Bible App does not access your music library. This description is required because a bundled library references media library APIs, even though this app does not use them.</string>
43
39
  </dict>
pubspec.yaml CHANGED
@@ -1,8 +1,7 @@
1
1
  name: only_bible_app
2
2
  description: The only bible app you will ever need
3
3
  publish_to: 'none'
4
- version: 3.6.0+19
4
+ version: 3.6.1+20
5
- resolution: workspace
6
5
 
7
6
  environment:
8
7
  sdk: '^3.12.2'
@@ -25,7 +24,7 @@ dependencies:
25
24
  file_picker: ^11.0.2
26
25
  flutter_azure_tts: ^1.0.0
27
26
  shared:
28
- path: ../../packages/shared
27
+ path: ../r2-backed-apps/packages/shared
29
28
 
30
29
  dev_dependencies:
31
30
  flutter_test:
test/actions_backup_test.dart CHANGED
@@ -1,5 +1,6 @@
1
1
  import "dart:convert";
2
2
 
3
+ import "package:flutter/material.dart";
3
4
  import "package:flutter_test/flutter_test.dart";
4
5
  import "package:only_bible_app/store/actions_backup.dart";
5
6
  import "package:only_bible_app/store/app_state.dart";
@@ -21,7 +22,7 @@ void main() {
21
22
  final bible = buildTestBible();
22
23
  final state = AppState(
23
24
  bible: bible,
24
- darkMode: true,
25
+ themeMode: ThemeMode.dark,
25
26
  fontSize: 22,
26
27
  highlights: {"0:0:0": 2},
27
28
  highlightHistory: [
@@ -32,7 +33,7 @@ void main() {
32
33
  final jsonString = buildBackupJson(state);
33
34
  final restored = await parseBackupState(jsonString, loadBibleFn: (_) async => bible);
34
35
 
35
- expect(restored.darkMode, true);
36
+ expect(restored.themeMode, ThemeMode.dark);
36
37
  expect(restored.fontSize, 22);
37
38
  expect(restored.highlights["0:0:0"], 2);
38
39
  expect(restored.highlightHistory.single.colorIndex, 2);
test/app_logic_test.dart CHANGED
@@ -1,6 +1,7 @@
1
1
  import "dart:io";
2
2
 
3
3
  import "package:async_redux/async_redux.dart";
4
+ import "package:flutter/material.dart";
4
5
  import "package:flutter_test/flutter_test.dart";
5
6
  import "package:path_provider_platform_interface/path_provider_platform_interface.dart";
6
7
  import "package:only_bible_app/env.dart";
@@ -183,7 +184,7 @@ void main() {
183
184
  savedBook: 1,
184
185
  savedChapter: 0,
185
186
  fontSize: 20,
186
- darkMode: true,
187
+ themeMode: ThemeMode.dark,
187
188
  highlightHistory: [
188
189
  HighlightHistoryEntry(book: 0, chapter: 0, verseIndex: 0, colorIndex: 1, timestamp: DateTime.utc(2026, 1, 1)),
189
190
  ],
@@ -192,7 +193,7 @@ void main() {
192
193
  expect(restored.savedBook, 1);
193
194
  expect(restored.savedChapter, 0);
194
195
  expect(restored.fontSize, 20);
195
- expect(restored.darkMode, true);
196
+ expect(restored.themeMode, ThemeMode.dark);
196
197
  expect(restored.highlightHistory.single.book, 0);
197
198
  });
198
199
 
@@ -208,7 +209,7 @@ void main() {
208
209
  final state = AppState.fromJson({}, bible);
209
210
  expect(state.savedBook, 0);
210
211
  expect(state.savedChapter, 0);
211
- expect(state.darkMode, false);
212
+ expect(state.themeMode, ThemeMode.system);
212
213
  expect(state.engTitles, false);
213
214
  });
214
215
  });
@@ -329,8 +330,8 @@ void main() {
329
330
  });
330
331
 
331
332
  group("theme", () {
332
- test("dark theme labelMedium uses the dark color scheme (not the light one)", () {
333
+ test("dark theme labelMedium (verse numbers) uses the dark error accent, matching the light theme's dedicated red", () {
333
- expect(AppTheme.dark.textTheme.labelMedium!.color, darkColorScheme.primary);
334
+ expect(AppTheme.dark.textTheme.labelMedium!.color, darkColorScheme.error);
334
335
  });
335
336
  });
336
337
 
@@ -356,14 +357,14 @@ void main() {
356
357
  test("persistDifference writes atomically, leaving no leftover temp file", () async {
357
358
  final bible = buildTestBible();
358
359
  final persistor = AppPersistor();
359
- final state = AppState(bible: bible, savedBook: 1, savedChapter: 0, darkMode: true);
360
+ final state = AppState(bible: bible, savedBook: 1, savedChapter: 0, themeMode: ThemeMode.dark);
360
361
 
361
362
  await persistor.persistDifference(lastPersistedState: null, newState: state);
362
363
 
363
364
  final json = await persistor.readJson();
364
365
  expect(json, isNotNull);
365
366
  expect(json!["savedBook"], 1);
366
- expect(json["darkMode"], true);
367
+ expect(json["themeMode"], "dark");
367
368
  expect(await File("${tempDir.path}/app_state.json.tmp").exists(), isFalse);
368
369
  });
369
370