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.


0228ae8Peter John 2026-08-08T15:28:12+05:30
major improvements please
Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: run test analyze macos ios android android-device list-devices setup
1
+ .PHONY: run test analyze macos ios android android-device list-devices setup release release-android release-ios release-macos
2
2
 
3
3
  setup:
4
4
  brew install fastlane
@@ -36,3 +36,21 @@ list-devices:
36
36
  @echo ""
37
37
  @echo "--- Android Emulators ---"
38
38
  emulator -list-avds 2>/dev/null || echo "No AVDs found (set ANDROID_HOME)"
39
+
40
+ # Builds, uploads and publishes to the Play Store, App Store and Mac App Store
41
+ # in one go. Screenshots are regenerated once here instead of once per
42
+ # platform (skip_screenshots:true tells each lane to reuse them).
43
+ release:
44
+ flutter test test/screenshot_test.dart --update-goldens
45
+ $(MAKE) release-android
46
+ $(MAKE) release-ios
47
+ $(MAKE) release-macos
48
+
49
+ release-android:
50
+ cd android && fastlane deploy skip_screenshots:true
51
+
52
+ release-ios:
53
+ cd ios && fastlane release skip_screenshots:true
54
+
55
+ release-macos:
56
+ cd macos && fastlane release skip_screenshots:true
android/fastlane/Fastfile CHANGED
@@ -36,10 +36,16 @@ platform :android do
36
36
  end
37
37
 
38
38
  desc "Deploy a new version to the Google Play"
39
- lane :deploy do
39
+ lane :deploy do |options|
40
- screenshots
40
+ screenshots unless options[:skip_screenshots]
41
- gradle(task: "clean assembleRelease")
41
+ # Invoking gradle directly (as the old `gradle(task: "clean assembleRelease")`
42
+ # did) skips `flutter pub get`'s plugin-registrant regeneration, which is
43
+ # what excludes dev-only plugins like integration_test from the release
44
+ # classpath. Building through `flutter build` runs that step first, so
45
+ # GeneratedPluginRegistrant.java is correct for a release build. It also
46
+ # produces an .aab (assembleRelease only produces an .apk).
47
+ sh("cd ../.. && flutter build appbundle --release")
42
- upload_to_play_store
48
+ upload
43
49
  end
44
50
 
45
51
  desc "Upload a pre-built AAB to the Google Play"
ios/Flutter/Debug.xcconfig CHANGED
@@ -1,2 +1 @@
1
- #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2
1
  #include "Generated.xcconfig"
ios/Flutter/Release.xcconfig CHANGED
@@ -1,2 +1 @@
1
- #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2
1
  #include "Generated.xcconfig"
ios/Podfile DELETED
@@ -1,43 +0,0 @@
1
- # Uncomment this line to define a global platform for your project
2
- platform :ios, '13.0'
3
-
4
- # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5
- ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6
-
7
- project 'Runner', {
8
- 'Debug' => :debug,
9
- 'Profile' => :release,
10
- 'Release' => :release,
11
- }
12
-
13
- def flutter_root
14
- generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15
- unless File.exist?(generated_xcode_build_settings_path)
16
- raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17
- end
18
-
19
- File.foreach(generated_xcode_build_settings_path) do |line|
20
- matches = line.match(/FLUTTER_ROOT\=(.*)/)
21
- return matches[1].strip if matches
22
- end
23
- raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24
- end
25
-
26
- require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27
-
28
- flutter_ios_podfile_setup
29
-
30
- target 'Runner' do
31
- use_frameworks!
32
-
33
- flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
34
- target 'RunnerTests' do
35
- inherit! :search_paths
36
- end
37
- end
38
-
39
- post_install do |installer|
40
- installer.pods_project.targets.each do |target|
41
- flutter_additional_ios_build_settings(target)
42
- end
43
- end
ios/Podfile.lock DELETED
@@ -1,22 +0,0 @@
1
- PODS:
2
- - Flutter (1.0.0)
3
- - flutter_soloud (0.0.1):
4
- - Flutter
5
-
6
- DEPENDENCIES:
7
- - Flutter (from `Flutter`)
8
- - flutter_soloud (from `.symlinks/plugins/flutter_soloud/ios`)
9
-
10
- EXTERNAL SOURCES:
11
- Flutter:
12
- :path: Flutter
13
- flutter_soloud:
14
- :path: ".symlinks/plugins/flutter_soloud/ios"
15
-
16
- SPEC CHECKSUMS:
17
- Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
18
- flutter_soloud: 272cc604453b6ca135f274045fb259e7c6426122
19
-
20
- PODFILE CHECKSUM: 251cb053df7158f337c0712f2ab29f4e0fa474ce
21
-
22
- COCOAPODS: 1.16.2
ios/Runner.xcodeproj/project.pbxproj CHANGED
@@ -9,15 +9,13 @@
9
9
  /* Begin PBXBuildFile section */
10
10
  1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11
11
  331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
12
- 391BE44176019420B8A3CC71 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47321D7A556A4A9BF16431AF /* Pods_RunnerTests.framework */; };
13
12
  3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
14
13
  74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
15
14
  7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
15
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
16
16
  97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
17
17
  97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
18
18
  97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
19
- B492E2663736AD73EF7204F6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 547E0FBE28272A54761BA898 /* Pods_Runner.framework */; };
20
- 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
21
19
  /* End PBXBuildFile section */
22
20
 
23
21
  /* Begin PBXContainerItemProxy section */
@@ -49,16 +47,11 @@
49
47
  331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
50
48
  331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
51
49
  3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
52
- 43630B2A8F1EB61852D12394 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
53
- 4420C6B24FA27C901C38F165 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
54
- 47321D7A556A4A9BF16431AF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
55
- 547E0FBE28272A54761BA898 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
56
- 6044DC6CC5B6EC34A4CDDEAB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
57
50
  74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
58
51
  74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
59
52
  7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
53
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
60
54
  7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
61
- 8445FAB3A6845463961F425E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
62
55
  9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
63
56
  9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
64
57
  97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -66,9 +59,6 @@
66
59
  97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
67
60
  97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
68
61
  97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
69
- B617937D6DA9BB0132F54E43 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
70
- D7CDE34E68CE03AC0364CCBE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
71
- 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
72
62
  /* End PBXFileReference section */
73
63
 
74
64
  /* Begin PBXFrameworksBuildPhase section */
@@ -76,7 +66,6 @@
76
66
  isa = PBXFrameworksBuildPhase;
77
67
  buildActionMask = 2147483647;
78
68
  files = (
79
- 391BE44176019420B8A3CC71 /* Pods_RunnerTests.framework in Frameworks */,
80
69
  );
81
70
  runOnlyForDeploymentPostprocessing = 0;
82
71
  };
@@ -85,7 +74,6 @@
85
74
  buildActionMask = 2147483647;
86
75
  files = (
87
76
  78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
88
- B492E2663736AD73EF7204F6 /* Pods_Runner.framework in Frameworks */,
89
77
  );
90
78
  runOnlyForDeploymentPostprocessing = 0;
91
79
  };
@@ -103,12 +91,6 @@
103
91
  818932C489EE28D703604D07 /* Pods */ = {
104
92
  isa = PBXGroup;
105
93
  children = (
106
- 6044DC6CC5B6EC34A4CDDEAB /* Pods-Runner.debug.xcconfig */,
107
- B617937D6DA9BB0132F54E43 /* Pods-Runner.release.xcconfig */,
108
- 43630B2A8F1EB61852D12394 /* Pods-Runner.profile.xcconfig */,
109
- 4420C6B24FA27C901C38F165 /* Pods-RunnerTests.debug.xcconfig */,
110
- D7CDE34E68CE03AC0364CCBE /* Pods-RunnerTests.release.xcconfig */,
111
- 8445FAB3A6845463961F425E /* Pods-RunnerTests.profile.xcconfig */,
112
94
  );
113
95
  path = Pods;
114
96
  sourceTree = "<group>";
@@ -133,7 +115,6 @@
133
115
  97C146EF1CF9000F007C117D /* Products */,
134
116
  331C8082294A63A400263BE5 /* RunnerTests */,
135
117
  818932C489EE28D703604D07 /* Pods */,
136
- AB430DFF6A3D37D341DBDBAC /* Frameworks */,
137
118
  );
138
119
  sourceTree = "<group>";
139
120
  };
@@ -162,15 +143,6 @@
162
143
  path = Runner;
163
144
  sourceTree = "<group>";
164
145
  };
165
- AB430DFF6A3D37D341DBDBAC /* Frameworks */ = {
166
- isa = PBXGroup;
167
- children = (
168
- 547E0FBE28272A54761BA898 /* Pods_Runner.framework */,
169
- 47321D7A556A4A9BF16431AF /* Pods_RunnerTests.framework */,
170
- );
171
- name = Frameworks;
172
- sourceTree = "<group>";
173
- };
174
146
  /* End PBXGroup section */
175
147
 
176
148
  /* Begin PBXNativeTarget section */
@@ -178,7 +150,6 @@
178
150
  isa = PBXNativeTarget;
179
151
  buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
180
152
  buildPhases = (
181
- 1B99EC6CBC943BDF479A1466 /* [CP] Check Pods Manifest.lock */,
182
153
  331C807D294A63A400263BE5 /* Sources */,
183
154
  331C807F294A63A400263BE5 /* Resources */,
184
155
  13AD50860C8A698628F45191 /* Frameworks */,
@@ -194,26 +165,24 @@
194
165
  productType = "com.apple.product-type.bundle.unit-test";
195
166
  };
196
167
  97C146ED1CF9000F007C117D /* Runner */ = {
197
- packageProductDependencies = (
198
- 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
199
- );
200
168
  isa = PBXNativeTarget;
201
169
  buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
202
170
  buildPhases = (
203
- 5DA2454F7F236B7C3EACFB0C /* [CP] Check Pods Manifest.lock */,
204
171
  9740EEB61CF901F6004384FC /* Run Script */,
205
172
  97C146EA1CF9000F007C117D /* Sources */,
206
173
  97C146EB1CF9000F007C117D /* Frameworks */,
207
174
  97C146EC1CF9000F007C117D /* Resources */,
208
175
  9705A1C41CF9048500538489 /* Embed Frameworks */,
209
176
  3B06AD1E1E4923F5004D2608 /* Thin Binary */,
210
- C1FC2023EB277B23DF78E0EB /* [CP] Embed Pods Frameworks */,
211
177
  );
212
178
  buildRules = (
213
179
  );
214
180
  dependencies = (
215
181
  );
216
182
  name = Runner;
183
+ packageProductDependencies = (
184
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
185
+ );
217
186
  productName = Runner;
218
187
  productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
219
188
  productType = "com.apple.product-type.application";
@@ -222,9 +191,6 @@
222
191
 
223
192
  /* Begin PBXProject section */
224
193
  97C146E61CF9000F007C117D /* Project object */ = {
225
- packageReferences = (
226
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
227
- );
228
194
  isa = PBXProject;
229
195
  attributes = {
230
196
  BuildIndependentTargetsInParallel = YES;
@@ -250,6 +216,9 @@
250
216
  Base,
251
217
  );
252
218
  mainGroup = 97C146E51CF9000F007C117D;
219
+ packageReferences = (
220
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
221
+ );
253
222
  productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
254
223
  projectDirPath = "";
255
224
  projectRoot = "";
@@ -282,28 +251,6 @@
282
251
  /* End PBXResourcesBuildPhase section */
283
252
 
284
253
  /* Begin PBXShellScriptBuildPhase section */
285
- 1B99EC6CBC943BDF479A1466 /* [CP] Check Pods Manifest.lock */ = {
286
- isa = PBXShellScriptBuildPhase;
287
- buildActionMask = 2147483647;
288
- files = (
289
- );
290
- inputFileListPaths = (
291
- );
292
- inputPaths = (
293
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
294
- "${PODS_ROOT}/Manifest.lock",
295
- );
296
- name = "[CP] Check Pods Manifest.lock";
297
- outputFileListPaths = (
298
- );
299
- outputPaths = (
300
- "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
301
- );
302
- runOnlyForDeploymentPostprocessing = 0;
303
- shellPath = /bin/sh;
304
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
305
- showEnvVarsInLog = 0;
306
- };
307
254
  3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
308
255
  isa = PBXShellScriptBuildPhase;
309
256
  alwaysOutOfDate = 1;
@@ -320,28 +267,6 @@
320
267
  shellPath = /bin/sh;
321
268
  shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
322
269
  };
323
- 5DA2454F7F236B7C3EACFB0C /* [CP] Check Pods Manifest.lock */ = {
324
- isa = PBXShellScriptBuildPhase;
325
- buildActionMask = 2147483647;
326
- files = (
327
- );
328
- inputFileListPaths = (
329
- );
330
- inputPaths = (
331
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
332
- "${PODS_ROOT}/Manifest.lock",
333
- );
334
- name = "[CP] Check Pods Manifest.lock";
335
- outputFileListPaths = (
336
- );
337
- outputPaths = (
338
- "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
339
- );
340
- runOnlyForDeploymentPostprocessing = 0;
341
- shellPath = /bin/sh;
342
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
343
- showEnvVarsInLog = 0;
344
- };
345
270
  9740EEB61CF901F6004384FC /* Run Script */ = {
346
271
  isa = PBXShellScriptBuildPhase;
347
272
  alwaysOutOfDate = 1;
@@ -357,23 +282,6 @@
357
282
  shellPath = /bin/sh;
358
283
  shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
359
284
  };
360
- C1FC2023EB277B23DF78E0EB /* [CP] Embed Pods Frameworks */ = {
361
- isa = PBXShellScriptBuildPhase;
362
- buildActionMask = 2147483647;
363
- files = (
364
- );
365
- inputFileListPaths = (
366
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
367
- );
368
- name = "[CP] Embed Pods Frameworks";
369
- outputFileListPaths = (
370
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
371
- );
372
- runOnlyForDeploymentPostprocessing = 0;
373
- shellPath = /bin/sh;
374
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
375
- showEnvVarsInLog = 0;
376
- };
377
285
  /* End PBXShellScriptBuildPhase section */
378
286
 
379
287
  /* Begin PBXSourcesBuildPhase section */
@@ -485,7 +393,7 @@
485
393
  CLANG_ENABLE_MODULES = YES;
486
394
  CODE_SIGN_IDENTITY = "Apple Development";
487
395
  CODE_SIGN_STYLE = Automatic;
488
- CURRENT_PROJECT_VERSION = 5;
396
+ CURRENT_PROJECT_VERSION = 8;
489
397
  DEVELOPMENT_TEAM = FXH8NUGFYY;
490
398
  ENABLE_BITCODE = NO;
491
399
  INFOPLIST_FILE = Runner/Info.plist;
@@ -504,11 +412,10 @@
504
412
  };
505
413
  331C8088294A63A400263BE5 /* Debug */ = {
506
414
  isa = XCBuildConfiguration;
507
- baseConfigurationReference = 4420C6B24FA27C901C38F165 /* Pods-RunnerTests.debug.xcconfig */;
508
415
  buildSettings = {
509
416
  BUNDLE_LOADER = "$(TEST_HOST)";
510
417
  CODE_SIGN_STYLE = Automatic;
511
- CURRENT_PROJECT_VERSION = 5;
418
+ CURRENT_PROJECT_VERSION = 8;
512
419
  GENERATE_INFOPLIST_FILE = YES;
513
420
  MARKETING_VERSION = 1.0;
514
421
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -522,11 +429,10 @@
522
429
  };
523
430
  331C8089294A63A400263BE5 /* Release */ = {
524
431
  isa = XCBuildConfiguration;
525
- baseConfigurationReference = D7CDE34E68CE03AC0364CCBE /* Pods-RunnerTests.release.xcconfig */;
526
432
  buildSettings = {
527
433
  BUNDLE_LOADER = "$(TEST_HOST)";
528
434
  CODE_SIGN_STYLE = Automatic;
529
- CURRENT_PROJECT_VERSION = 5;
435
+ CURRENT_PROJECT_VERSION = 8;
530
436
  GENERATE_INFOPLIST_FILE = YES;
531
437
  MARKETING_VERSION = 1.0;
532
438
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -538,11 +444,10 @@
538
444
  };
539
445
  331C808A294A63A400263BE5 /* Profile */ = {
540
446
  isa = XCBuildConfiguration;
541
- baseConfigurationReference = 8445FAB3A6845463961F425E /* Pods-RunnerTests.profile.xcconfig */;
542
447
  buildSettings = {
543
448
  BUNDLE_LOADER = "$(TEST_HOST)";
544
449
  CODE_SIGN_STYLE = Automatic;
545
- CURRENT_PROJECT_VERSION = 5;
450
+ CURRENT_PROJECT_VERSION = 8;
546
451
  GENERATE_INFOPLIST_FILE = YES;
547
452
  MARKETING_VERSION = 1.0;
548
453
  PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
@@ -671,7 +576,7 @@
671
576
  CLANG_ENABLE_MODULES = YES;
672
577
  CODE_SIGN_IDENTITY = "Apple Development";
673
578
  CODE_SIGN_STYLE = Automatic;
674
- CURRENT_PROJECT_VERSION = 5;
579
+ CURRENT_PROJECT_VERSION = 8;
675
580
  DEVELOPMENT_TEAM = FXH8NUGFYY;
676
581
  ENABLE_BITCODE = NO;
677
582
  INFOPLIST_FILE = Runner/Info.plist;
@@ -698,7 +603,7 @@
698
603
  CODE_SIGN_IDENTITY = "Apple Development";
699
604
  "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
700
605
  CODE_SIGN_STYLE = Manual;
701
- CURRENT_PROJECT_VERSION = 5;
606
+ CURRENT_PROJECT_VERSION = 8;
702
607
  DEVELOPMENT_TEAM = "";
703
608
  "DEVELOPMENT_TEAM[sdk=iphoneos*]" = FXH8NUGFYY;
704
609
  ENABLE_BITCODE = NO;
@@ -751,12 +656,14 @@
751
656
  defaultConfigurationName = Release;
752
657
  };
753
658
  /* End XCConfigurationList section */
659
+
754
660
  /* Begin XCLocalSwiftPackageReference section */
755
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
661
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
756
662
  isa = XCLocalSwiftPackageReference;
757
663
  relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
758
664
  };
759
665
  /* End XCLocalSwiftPackageReference section */
666
+
760
667
  /* Begin XCSwiftPackageProductDependency section */
761
668
  78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
762
669
  isa = XCSwiftPackageProductDependency;
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved DELETED
@@ -1,59 +0,0 @@
1
- {
2
- "pins" : [
3
- {
4
- "identity" : "dkcamera",
5
- "kind" : "remoteSourceControl",
6
- "location" : "https://github.com/zhangao0086/DKCamera",
7
- "state" : {
8
- "branch" : "master",
9
- "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
10
- }
11
- },
12
- {
13
- "identity" : "dkimagepickercontroller",
14
- "kind" : "remoteSourceControl",
15
- "location" : "https://github.com/zhangao0086/DKImagePickerController",
16
- "state" : {
17
- "branch" : "4.3.9",
18
- "revision" : "0bdfeacefa308545adde07bef86e349186335915"
19
- }
20
- },
21
- {
22
- "identity" : "dkphotogallery",
23
- "kind" : "remoteSourceControl",
24
- "location" : "https://github.com/zhangao0086/DKPhotoGallery",
25
- "state" : {
26
- "branch" : "master",
27
- "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
28
- }
29
- },
30
- {
31
- "identity" : "sdwebimage",
32
- "kind" : "remoteSourceControl",
33
- "location" : "https://github.com/SDWebImage/SDWebImage",
34
- "state" : {
35
- "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
36
- "version" : "5.21.7"
37
- }
38
- },
39
- {
40
- "identity" : "swiftygif",
41
- "kind" : "remoteSourceControl",
42
- "location" : "https://github.com/kirualex/SwiftyGif.git",
43
- "state" : {
44
- "revision" : "4430cbc148baa3907651d40562d96325426f409a",
45
- "version" : "5.4.5"
46
- }
47
- },
48
- {
49
- "identity" : "tocropviewcontroller",
50
- "kind" : "remoteSourceControl",
51
- "location" : "https://github.com/TimOliver/TOCropViewController",
52
- "state" : {
53
- "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
54
- "version" : "2.8.0"
55
- }
56
- }
57
- ],
58
- "version" : 2
59
- }
ios/Runner.xcworkspace/contents.xcworkspacedata CHANGED
@@ -4,7 +4,4 @@
4
4
  <FileRef
5
5
  location = "group:Runner.xcodeproj">
6
6
  </FileRef>
7
- <FileRef
8
- location = "group:Pods/Pods.xcodeproj">
9
- </FileRef>
10
7
  </Workspace>
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved DELETED
@@ -1,59 +0,0 @@
1
- {
2
- "pins" : [
3
- {
4
- "identity" : "dkcamera",
5
- "kind" : "remoteSourceControl",
6
- "location" : "https://github.com/zhangao0086/DKCamera",
7
- "state" : {
8
- "branch" : "master",
9
- "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
10
- }
11
- },
12
- {
13
- "identity" : "dkimagepickercontroller",
14
- "kind" : "remoteSourceControl",
15
- "location" : "https://github.com/zhangao0086/DKImagePickerController",
16
- "state" : {
17
- "branch" : "4.3.9",
18
- "revision" : "0bdfeacefa308545adde07bef86e349186335915"
19
- }
20
- },
21
- {
22
- "identity" : "dkphotogallery",
23
- "kind" : "remoteSourceControl",
24
- "location" : "https://github.com/zhangao0086/DKPhotoGallery",
25
- "state" : {
26
- "branch" : "master",
27
- "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
28
- }
29
- },
30
- {
31
- "identity" : "sdwebimage",
32
- "kind" : "remoteSourceControl",
33
- "location" : "https://github.com/SDWebImage/SDWebImage",
34
- "state" : {
35
- "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
36
- "version" : "5.21.7"
37
- }
38
- },
39
- {
40
- "identity" : "swiftygif",
41
- "kind" : "remoteSourceControl",
42
- "location" : "https://github.com/kirualex/SwiftyGif.git",
43
- "state" : {
44
- "revision" : "4430cbc148baa3907651d40562d96325426f409a",
45
- "version" : "5.4.5"
46
- }
47
- },
48
- {
49
- "identity" : "tocropviewcontroller",
50
- "kind" : "remoteSourceControl",
51
- "location" : "https://github.com/TimOliver/TOCropViewController",
52
- "state" : {
53
- "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
54
- "version" : "2.8.0"
55
- }
56
- }
57
- ],
58
- "version" : 2
59
- }
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>5</string>
26
+ <string>8</string>
27
27
  <key>LSRequiresIPhoneOS</key>
28
28
  <true/>
29
29
  <key>UIApplicationSceneManifest</key>
@@ -70,9 +70,5 @@
70
70
  <false/>
71
71
  <key>ITSAppUsesNonExemptEncryption</key>
72
72
  <false/>
73
- <key>NSPhotoLibraryUsageDescription</key>
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>NSAppleMusicUsageDescription</key>
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>
77
73
  </dict>
78
74
  </plist>
ios/fastlane/Fastfile CHANGED
@@ -40,8 +40,8 @@ platform :ios do
40
40
  end
41
41
 
42
42
  desc "Push a new release build to the App Store"
43
- lane :release do
43
+ lane :release do |options|
44
- screenshots
44
+ screenshots unless options[:skip_screenshots]
45
45
  certs
46
46
  increment_build_number(xcodeproj: "Runner.xcodeproj")
47
47
  build_app(
@@ -54,7 +54,11 @@ platform :ios do
54
54
  }
55
55
  )
56
56
  api_key = api_key()
57
+ upload_to_app_store(
58
+ api_key: api_key,
57
- upload_to_app_store(api_key: api_key, overwrite_screenshots: true)
59
+ overwrite_screenshots: true,
60
+ precheck_include_in_app_purchases: false
61
+ )
58
62
  end
59
63
 
60
64
  desc "Upload a pre-built IPA to the App Store"
lib/app.dart CHANGED
@@ -5,7 +5,7 @@ import "package:only_bible_app/home.dart";
5
5
  import "package:only_bible_app/store/app_navigator.dart";
6
6
  import "package:only_bible_app/store/app_state.dart";
7
7
  import "package:only_bible_app/store/actions_navigation.dart";
8
- import "package:shared/shared.dart";
8
+ import "package:only_bible_app/theme.dart";
9
9
 
10
10
  class App extends StatelessWidget {
11
11
  final GlobalKey<NavigatorState> globalNavigatorKey;
lib/dialog.dart CHANGED
@@ -6,7 +6,7 @@ import "package:only_bible_app/gen/bible.gen.dart";
6
6
  import "package:only_bible_app/store/actions_navigation.dart";
7
7
  import "package:only_bible_app/store/app_navigator.dart";
8
8
  import "package:only_bible_app/store/app_state.dart";
9
- export "package:shared/shared.dart" show showAlert, showError, showConfirm;
9
+ export "package:only_bible_app/dialogs.dart" show showAlert, showError, showConfirm;
10
10
 
11
11
  void showBibleSelectDialog(BuildContext context, Bible bible) {
12
12
  showDialog(
lib/dialogs.dart ADDED
@@ -0,0 +1,66 @@
1
+ import "package:flutter/material.dart";
2
+
3
+ void showAlert(BuildContext context, String title, String message) {
4
+ showDialog(
5
+ context: context,
6
+ barrierColor: Colors.black54,
7
+ builder: (_) {
8
+ return AlertDialog(
9
+ title: Text(title),
10
+ content: Text(message),
11
+ actionsAlignment: MainAxisAlignment.center,
12
+ actionsOverflowButtonSpacing: 8.0,
13
+ actions: [
14
+ TextButton(
15
+ onPressed: () {
16
+ Navigator.of(context).pop();
17
+ },
18
+ child: const Text("OK"),
19
+ ),
20
+ ],
21
+ );
22
+ },
23
+ );
24
+ }
25
+
26
+ void showError(BuildContext context, String message) {
27
+ showAlert(context, "Error", message);
28
+ }
29
+
30
+ Future<bool> showConfirm(
31
+ BuildContext context,
32
+ String title,
33
+ String message,
34
+ ) async {
35
+ const buttonStyle = ButtonStyle(
36
+ padding: WidgetStatePropertyAll(
37
+ EdgeInsets.symmetric(horizontal: 20, vertical: 10),
38
+ ),
39
+ textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 13)),
40
+ );
41
+ final result = await showDialog<bool>(
42
+ context: context,
43
+ barrierColor: Colors.black54,
44
+ builder: (dialogContext) {
45
+ return AlertDialog(
46
+ title: Text(title),
47
+ content: Text(message),
48
+ actionsAlignment: MainAxisAlignment.center,
49
+ actionsOverflowButtonSpacing: 8.0,
50
+ actions: [
51
+ TextButton(
52
+ style: buttonStyle,
53
+ onPressed: () => Navigator.of(dialogContext).pop(false),
54
+ child: const Text("Cancel"),
55
+ ),
56
+ FilledButton(
57
+ style: buttonStyle,
58
+ onPressed: () => Navigator.of(dialogContext).pop(true),
59
+ child: const Text("Continue"),
60
+ ),
61
+ ],
62
+ );
63
+ },
64
+ );
65
+ return result ?? false;
66
+ }
lib/error_reporting.dart ADDED
@@ -0,0 +1,147 @@
1
+ import "dart:async";
2
+ import "dart:convert";
3
+
4
+ import "package:flutter/foundation.dart";
5
+ import "package:flutter/material.dart";
6
+ import "package:flutter/scheduler.dart";
7
+ import "package:http/http.dart" as http;
8
+
9
+ /// Sends (or otherwise records) a single error report. Implementations
10
+ /// should be best-effort — a failed report must never throw back into the
11
+ /// error handler that triggered it.
12
+ typedef ErrorReportSender =
13
+ Future<void> Function(String message, StackTrace? stack);
14
+
15
+ /// Global crash/error reporting: wires [FlutterError.onError] (framework
16
+ /// errors) and [PlatformDispatcher.onError] (uncaught async/platform
17
+ /// errors), shows a user-consent dialog before reporting in release builds,
18
+ /// and just logs to the console in debug builds.
19
+ class ErrorReporting {
20
+ ErrorReporting._();
21
+
22
+ static GlobalKey<NavigatorState>? _navigatorKey;
23
+ static ErrorReportSender? _onReport;
24
+ static Set<String> _ignoredSubstrings = const {};
25
+
26
+ /// Call once at app startup, after `runApp`'s `navigatorKey` exists.
27
+ static void install({
28
+ required GlobalKey<NavigatorState> navigatorKey,
29
+ required ErrorReportSender onReport,
30
+ Set<String> ignoredSubstrings = const {},
31
+ }) {
32
+ _navigatorKey = navigatorKey;
33
+ _onReport = onReport;
34
+ _ignoredSubstrings = ignoredSubstrings;
35
+
36
+ FlutterError.onError = (errorDetails) {
37
+ FlutterError.presentError(errorDetails);
38
+ _handle(errorDetails.exception.toString(), errorDetails.stack);
39
+ };
40
+
41
+ PlatformDispatcher.instance.onError = (error, stack) {
42
+ _handle(error.toString(), stack);
43
+ return true;
44
+ };
45
+ }
46
+
47
+ /// Manually reports [message] through the same sender configured via
48
+ /// [install], bypassing the consent dialog — for call sites that already
49
+ /// caught and handled a specific error and just want it recorded (e.g.
50
+ /// only-bible-app's audio-playback failure handler).
51
+ static Future<void> report(String message, StackTrace? stack) async {
52
+ if (kDebugMode) {
53
+ debugPrint("Error: $message");
54
+ debugPrint("$stack");
55
+ return;
56
+ }
57
+ await _onReport?.call(message, stack);
58
+ }
59
+
60
+ static void _handle(String message, StackTrace? stack) {
61
+ if (kDebugMode) {
62
+ debugPrint("Error: $message");
63
+ debugPrint("$stack");
64
+ return;
65
+ }
66
+ if (_ignoredSubstrings.any(message.contains)) {
67
+ // Known framework noise: still recorded for visibility, just not
68
+ // worth interrupting the user with a dialog they can't act on.
69
+ unawaited(_onReport?.call(message, stack));
70
+ return;
71
+ }
72
+ SchedulerBinding.instance.addPostFrameCallback((_) {
73
+ final context = _navigatorKey?.currentContext;
74
+ if (context == null || !context.mounted) return;
75
+ _showReportDialog(context, message, stack);
76
+ });
77
+ }
78
+
79
+ static void _showReportDialog(
80
+ BuildContext context,
81
+ String message,
82
+ StackTrace? stack,
83
+ ) {
84
+ showDialog<void>(
85
+ context: context,
86
+ barrierColor: Colors.black54,
87
+ builder: (dialogContext) => AlertDialog(
88
+ title: const Text("Alert"),
89
+ content: const Text(
90
+ "An error has occurred. Do you want to report this error to us?",
91
+ ),
92
+ actionsAlignment: MainAxisAlignment.end,
93
+ actionsOverflowButtonSpacing: 8.0,
94
+ actions: [
95
+ TextButton(
96
+ onPressed: () {
97
+ Navigator.of(dialogContext).pop();
98
+ unawaited(_onReport?.call(message, stack));
99
+ },
100
+ child: const Text("Yes"),
101
+ ),
102
+ TextButton(
103
+ onPressed: () => Navigator.of(dialogContext).pop(),
104
+ child: const Text("No"),
105
+ ),
106
+ ],
107
+ ),
108
+ );
109
+ }
110
+ }
111
+
112
+ /// Builds an [ErrorReportSender] that POSTs to the shared `error-reporter`
113
+ /// Cloudflare Worker (see workers/error-reporter/), which emails the report
114
+ /// via Cloudflare Email Sending. [sharedSecret], if provided, is sent as
115
+ /// `X-Report-Secret` — this is NOT a real security boundary (it's embedded
116
+ /// client-side, same as any app secret), it only filters out generic bots
117
+ /// blindly probing the endpoint. Real abuse resistance is the Worker's own
118
+ /// rate limiter.
119
+ ErrorReportSender httpErrorReportSender({
120
+ required Uri endpoint,
121
+ required String appName,
122
+ String? sharedSecret,
123
+ String? appVersion,
124
+ String? buildNumber,
125
+ }) {
126
+ return (message, stack) async {
127
+ try {
128
+ await http.post(
129
+ endpoint,
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ if (sharedSecret != null) "X-Report-Secret": sharedSecret,
133
+ },
134
+ body: jsonEncode({
135
+ "app": appName,
136
+ "message": message,
137
+ "stack": stack?.toString(),
138
+ "platform": defaultTargetPlatform.name,
139
+ "appVersion": appVersion,
140
+ "buildNumber": buildNumber,
141
+ }),
142
+ );
143
+ } catch (_) {
144
+ // Best-effort — a failed report must not cascade into another error.
145
+ }
146
+ };
147
+ }
lib/main.dart CHANGED
@@ -9,8 +9,8 @@ import "package:only_bible_app/app.dart";
9
9
  import "package:only_bible_app/env.dart";
10
10
  import "package:only_bible_app/store/app_persistor.dart";
11
11
  import "package:only_bible_app/store/app_state.dart";
12
+ import "package:only_bible_app/error_reporting.dart";
12
13
  import "package:only_bible_app/utils.dart";
13
- import "package:shared/shared.dart" show ErrorReporting, httpErrorReportSender;
14
14
 
15
15
  // TODO: replace with the deployed error-reporter Worker's actual URL (see
16
16
  // workers/error-reporter/) — either its workers.dev subdomain or a custom
lib/store/actions_backup.dart CHANGED
@@ -2,13 +2,15 @@ import "dart:convert";
2
2
  import "dart:io";
3
3
 
4
4
  import "package:async_redux/async_redux.dart";
5
- import "package:file_picker/file_picker.dart";
5
+ import "package:file_selector/file_selector.dart";
6
6
  import "package:flutter/foundation.dart";
7
7
  import "package:flutter/material.dart";
8
8
  import "package:only_bible_app/dialog.dart";
9
9
  import "package:only_bible_app/gen/bible.gen.dart";
10
10
  import "package:only_bible_app/store/app_state.dart";
11
11
  import "package:only_bible_app/utils.dart";
12
+ import "package:path_provider/path_provider.dart";
13
+ import "package:share_plus/share_plus.dart";
12
14
 
13
15
  String buildBackupFileName([DateTime? now]) {
14
16
  final n = now ?? DateTime.now();
@@ -70,21 +72,33 @@ class ExportAppStateAction extends ReduxAction<AppState> {
70
72
  Future<AppState?> reduce() async {
71
73
  final jsonString = buildBackupJson(state);
72
74
  final fileName = buildBackupFileName();
73
- // Desktop (macOS/Windows/Linux): saveFile opens a save dialog and returns
75
+ // file_selector's save-location dialog isn't supported on Android/iOS
74
- // the chosen path; we then write the file manually.
75
- // Mobile (iOS/Android) and web: pass bytes so the plugin/browser handles
76
+ // (only desktop and web), so mobile exports go through the share sheet
76
- // writing before presenting the share/download UI.
77
+ // (Save to Files, AirDrop, etc.) instead.
77
- final isDesktop = !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
78
- final encodedBytes = isDesktop ? null : utf8.encode(jsonString);
78
+ final canPickSaveLocation = kIsWeb || isDesktop;
79
- String? path;
80
79
  try {
80
+ if (canPickSaveLocation) {
81
- path = await FilePicker.saveFile(
81
+ final location = await getSaveLocation(
82
- fileName: fileName,
82
+ suggestedName: fileName,
83
- bytes: encodedBytes,
83
+ acceptedTypeGroups: const [
84
+ XTypeGroup(label: "JSON", extensions: ["json"]),
85
+ ],
84
- );
86
+ );
85
- if (path == null) return null; // user cancelled the dialog
87
+ if (location == null) return null; // user cancelled
88
+ final file = XFile.fromData(
89
+ utf8.encode(jsonString),
90
+ mimeType: "application/json",
91
+ name: fileName,
92
+ );
93
+ await file.saveTo(location.path);
86
- if (isDesktop) {
94
+ } else {
95
+ final dir = await getTemporaryDirectory();
96
+ final path = "${dir.path}/$fileName";
87
97
  await File(path).writeAsString(jsonString);
98
+ final result = await SharePlus.instance.share(
99
+ ShareParams(files: [XFile(path)]),
100
+ );
101
+ if (result.status == ShareResultStatus.dismissed) return null;
88
102
  }
89
103
  } catch (_) {
90
104
  if (buildContext.mounted) {
@@ -93,7 +107,11 @@ class ExportAppStateAction extends ReduxAction<AppState> {
93
107
  return null;
94
108
  }
95
109
  if (buildContext.mounted) {
110
+ showAlert(
111
+ buildContext,
112
+ "Backup Exported",
96
- showAlert(buildContext, "Backup Exported", "Your backup was saved successfully.");
113
+ "Your backup was saved successfully.",
114
+ );
97
115
  }
98
116
  return null;
99
117
  }
@@ -114,25 +132,22 @@ class ImportAppStateAction extends ReduxAction<AppState> {
114
132
  if (!confirmed) return null;
115
133
  if (!buildContext.mounted) return null;
116
134
 
117
- final result = await FilePicker.pickFiles(
135
+ final file = await openFile(
118
- type: FileType.custom,
136
+ acceptedTypeGroups: const [
119
- allowedExtensions: ["json"],
137
+ XTypeGroup(label: "JSON", extensions: ["json"]),
120
- withData: true,
138
+ ],
121
139
  );
122
- if (result == null || result.files.isEmpty) return null; // user cancelled
140
+ if (file == null) return null; // user cancelled
123
-
124
- final bytes = result.files.single.bytes;
125
- if (bytes == null) {
126
- if (buildContext.mounted) {
127
- showError(buildContext, "That file isn't a valid backup");
128
- }
129
- return null;
130
- }
131
141
 
132
142
  try {
143
+ final bytes = await file.readAsBytes();
133
144
  final newState = await parseBackupState(utf8.decode(bytes));
134
145
  if (buildContext.mounted) {
146
+ showAlert(
147
+ buildContext,
148
+ "Backup Imported",
135
- showAlert(buildContext, "Backup Imported", "Your backup was restored successfully.");
149
+ "Your backup was restored successfully.",
150
+ );
136
151
  }
137
152
  return newState;
138
153
  } on BackupParseException catch (err) {
@@ -140,6 +155,11 @@ class ImportAppStateAction extends ReduxAction<AppState> {
140
155
  showError(buildContext, err.message);
141
156
  }
142
157
  return null;
158
+ } catch (_) {
159
+ if (buildContext.mounted) {
160
+ showError(buildContext, "That file isn't a valid backup");
161
+ }
162
+ return null;
143
163
  }
144
164
  }
145
165
  }
lib/store/actions_state.dart CHANGED
@@ -1,13 +1,12 @@
1
1
  import "dart:developer";
2
2
 
3
3
  import "package:async_redux/async_redux.dart";
4
- import "package:flutter_soloud/flutter_soloud.dart";
5
4
  import "package:flutter/material.dart";
6
5
  import "package:only_bible_app/dialog.dart";
6
+ import "package:only_bible_app/error_reporting.dart";
7
7
  import "package:only_bible_app/gen/bible.gen.dart";
8
8
  import "package:only_bible_app/store/app_state.dart";
9
9
  import "package:only_bible_app/utils.dart";
10
- import "package:shared/shared.dart" show ErrorReporting;
11
10
 
12
11
  class ToggleEngTitlesAction extends ReduxAction<AppState> {
13
12
  @override
@@ -79,32 +78,21 @@ class TogglePlayAction extends ReduxAction<AppState> {
79
78
  final pathname = "${bible.name!}_${v.book}_${v.chapter}_${v.index}";
80
79
  try {
81
80
  final data = await convertText(audioVoice, v.text ?? "");
82
- if (!SoLoud.instance.isInitialized) break;
81
+ if (!isAudioPlaybackInitialized) break;
83
- final stream = SoLoud.instance.setBufferStream(
84
- bufferingType: BufferingType.preserved,
85
- sampleRate: 24000,
86
- channels: Channels.mono,
87
- format: BufferType.s16le,
88
- );
89
- SoLoud.instance.addAudioDataStream(stream, data);
82
+ await playAudioClip(data);
90
- SoLoud.instance.setDataIsEnded(stream);
83
+ if (!isAudioPlaybackInitialized) break;
91
- final handle = await SoLoud.instance.play(stream);
92
- // Wait for playback to complete
93
- await stream.soundEvents.firstWhere(
94
- (e) =>
95
- (e.event == SoundEventType.handleIsNoMoreValid &&
96
- e.handle == handle) ||
97
- e.event == SoundEventType.soundDisposed,
98
- );
99
84
  } catch (err) {
100
- // If sources were disposed (user stopped playback), don't treat as error
85
+ // If playback was disposed (user stopped playback), don't treat as error
101
- if (!SoLoud.instance.isInitialized) break;
86
+ if (!isAudioPlaybackInitialized) break;
102
87
  log(
103
88
  "Could not play audio",
104
89
  name: "play",
105
90
  error: (err.toString(), pathname),
106
91
  );
92
+ await ErrorReporting.report(
107
- await ErrorReporting.report((err.toString(), pathname).toString(), null);
93
+ (err.toString(), pathname).toString(),
94
+ null,
95
+ );
108
96
  if (buildContext.mounted) {
109
97
  showError(buildContext, audioError);
110
98
  }
@@ -137,12 +125,15 @@ class SelectVerseAction extends ReduxAction<AppState> {
137
125
  );
138
126
  final newVerses = isSelected
139
127
  ? state.selectedVerses
128
+ .where(
129
+ (it) =>
130
+ it.book != verse.book ||
140
- .where((it) => it.book != verse.book || it.chapter != verse.chapter || it.index != verse.index)
131
+ it.chapter != verse.chapter ||
132
+ it.index != verse.index,
133
+ )
141
- .toList()
134
+ .toList()
142
135
  : [...state.selectedVerses, verse];
143
- return state.copy(
144
- selectedVerses: newVerses,
136
+ return state.copy(selectedVerses: newVerses);
145
- );
146
137
  }
147
138
  }
148
139
 
@@ -173,8 +164,14 @@ class SetHighlightAction extends ReduxAction<AppState> {
173
164
  }).toList();
174
165
  // Keep last 500 entries
175
166
  final updatedHistory = [...state.highlightHistory, ...newEntries];
167
+ final trimmed = updatedHistory.length > 500
176
- final trimmed = updatedHistory.length > 500 ? updatedHistory.sublist(updatedHistory.length - 500) : updatedHistory;
168
+ ? updatedHistory.sublist(updatedHistory.length - 500)
169
+ : updatedHistory;
170
+ return state.copy(
171
+ highlights: updated,
172
+ selectedVerses: [],
177
- return state.copy(highlights: updated, selectedVerses: [], highlightHistory: trimmed);
173
+ highlightHistory: trimmed,
174
+ );
178
175
  }
179
176
  }
180
177
 
lib/theme.dart ADDED
@@ -0,0 +1,343 @@
1
+ import "package:flutter/material.dart";
2
+
3
+ const lightColorScheme = ColorScheme(
4
+ brightness: Brightness.light,
5
+ primary: Color.fromARGB(255, 100, 127, 203),
6
+ onPrimary: Color(0xFFFFFFFF),
7
+ primaryContainer: Color(0xFFDAE2FF),
8
+ onPrimaryContainer: Color(0xFF001848),
9
+ secondary: Color(0xFF4C5C92),
10
+ onSecondary: Color(0xFFFFFFFF),
11
+ secondaryContainer: Color(0xFFDCE1FF),
12
+ onSecondaryContainer: Color(0xFF02174B),
13
+ tertiary: Color(0xFF7D4E7D),
14
+ onTertiary: Color(0xFFFFFFFF),
15
+ tertiaryContainer: Color(0xFFFFD6FA),
16
+ onTertiaryContainer: Color(0xFF320935),
17
+ error: Color(0xFFBA1A1A),
18
+ onError: Color(0xFFFFFFFF),
19
+ errorContainer: Color(0xFFFFDAD6),
20
+ onErrorContainer: Color(0xFF410002),
21
+ surface: Color(0xFFFFFFFF),
22
+ onSurface: Color(0xFF000000),
23
+ onSurfaceVariant: Color(0xFF45464F),
24
+ outline: Color.fromARGB(255, 157, 160, 171),
25
+ outlineVariant: Color(0xFFC5C6D0),
26
+ shadow: Color.fromARGB(255, 88, 85, 85),
27
+ scrim: Color(0xFF000000),
28
+ inverseSurface: Color(0xFF2F3036),
29
+ onInverseSurface: Color(0xFFF1F0F7),
30
+ inversePrimary: Color(0xFFB2C5FF),
31
+ surfaceDim: Color(0xFFDAD9E0),
32
+ surfaceBright: Color(0xFFFAF8FF),
33
+ surfaceContainerLowest: Color(0xFFFFFFFF),
34
+ surfaceContainerLow: Color(0xFFF4F3FA),
35
+ surfaceContainer: Color(0xFFEEEDF4),
36
+ surfaceContainerHigh: Color(0xFFE8E7EF),
37
+ surfaceContainerHighest: Color(0xFFE3E2E9),
38
+ );
39
+
40
+ const darkColorScheme = ColorScheme(
41
+ brightness: Brightness.dark,
42
+ // Accent: Claude Code permission/suggestion blue-purple — rgb(177,185,249)
43
+ primary: Color(0xFFB1B9F9),
44
+ onPrimary: Color(0xFF1A2050),
45
+ primaryContainer: Color(0xFF2A3580),
46
+ onPrimaryContainer: Color(0xFFDDE0FF),
47
+ secondary: Color(0xFFB1B9F9),
48
+ onSecondary: Color(0xFF1A2050),
49
+ secondaryContainer: Color(0xFF2A3580),
50
+ onSecondaryContainer: Color(0xFFDDE0FF),
51
+ // Tertiary: Claude Code brand orange — rgb(215,119,87)
52
+ tertiary: Color(0xFFD77757),
53
+ onTertiary: Color(0xFF4A1A00),
54
+ tertiaryContainer: Color(0xFF6B3020),
55
+ onTertiaryContainer: Color(0xFFFFDBCC),
56
+ // Error: Claude Code error — rgb(255,107,128)
57
+ error: Color(0xFFFF6B80),
58
+ onError: Color(0xFF690005),
59
+ errorContainer: Color(0xFF8C1D18),
60
+ onErrorContainer: Color(0xFFFFDAD6),
61
+ // Neutral dark gray — matches Claude Code desktop aesthetic (no blue tint)
62
+ surface: Color(0xFF1E1E1E),
63
+ onSurface: Color(0xFFE8E8E8),
64
+ // Secondary text: Claude Code inactive — rgb(153,153,153)
65
+ onSurfaceVariant: Color(0xFF999999),
66
+ outline: Color(0xFF666666),
67
+ outlineVariant: Color(0xFF3A3A3A),
68
+ shadow: Color(0xFF000000),
69
+ scrim: Color(0xFF000000),
70
+ inverseSurface: Color(0xFFE3E3E3),
71
+ onInverseSurface: Color(0xFF303030),
72
+ inversePrimary: Color(0xFF4A5AA0),
73
+ surfaceDim: Color(0xFF181818),
74
+ surfaceBright: Color(0xFF323232),
75
+ surfaceContainerLowest: Color(0xFF141414),
76
+ surfaceContainerLow: Color(0xFF222222),
77
+ surfaceContainer: Color(0xFF262626),
78
+ surfaceContainerHigh: Color(0xFF2C2C2C),
79
+ // Elevated panels: Claude Code userMessageBackground — rgb(55,55,55)
80
+ surfaceContainerHighest: Color(0xFF373737),
81
+ );
82
+
83
+ class AppTheme {
84
+ AppTheme._();
85
+
86
+ static final ThemeData light = ThemeData(
87
+ brightness: Brightness.light,
88
+ colorScheme: lightColorScheme,
89
+ useMaterial3: true,
90
+ applyElevationOverlayColor: false,
91
+ textSelectionTheme: TextSelectionThemeData(
92
+ cursorColor: Colors.black,
93
+ selectionHandleColor: Colors.black,
94
+ selectionColor: Colors.grey.shade200,
95
+ ),
96
+ inputDecorationTheme: const InputDecorationTheme(
97
+ focusColor: Colors.transparent,
98
+ hoverColor: Colors.transparent,
99
+ activeIndicatorBorder: BorderSide(color: Colors.black),
100
+ hintStyle: TextStyle(color: Colors.grey),
101
+ ),
102
+ appBarTheme: AppBarTheme(
103
+ backgroundColor: lightColorScheme.surface,
104
+ foregroundColor: lightColorScheme.onSurface,
105
+ elevation: 1,
106
+ // Material3 otherwise blends in a colorScheme.surfaceTint overlay
107
+ // proportional to elevation, making the AppBar a visibly different
108
+ // shade from the flat Scaffold background behind it.
109
+ surfaceTintColor: Colors.transparent,
110
+ ),
111
+ bottomSheetTheme: BottomSheetThemeData(
112
+ elevation: 10,
113
+ backgroundColor: lightColorScheme.surface,
114
+ shadowColor: lightColorScheme.scrim,
115
+ showDragHandle: true,
116
+ dragHandleSize: Size(50, 3),
117
+ ),
118
+ dialogTheme: const DialogThemeData(
119
+ elevation: 10,
120
+ shadowColor: Colors.black,
121
+ backgroundColor: Colors.white,
122
+ surfaceTintColor: Colors.white,
123
+ shape: Border(top: BorderSide(width: 1.5, color: Colors.black)),
124
+ ),
125
+ iconButtonTheme: IconButtonThemeData(
126
+ style: IconButton.styleFrom(enableFeedback: true),
127
+ ),
128
+ textButtonTheme: TextButtonThemeData(
129
+ style: TextButton.styleFrom(
130
+ enableFeedback: true,
131
+ padding: EdgeInsets.zero,
132
+ shape: RoundedRectangleBorder(
133
+ borderRadius: BorderRadius.circular(4),
134
+ side: BorderSide(color: Colors.black, width: 1),
135
+ ),
136
+ elevation: 2,
137
+ shadowColor: Colors.black,
138
+ backgroundColor: lightColorScheme.surfaceContainerLow,
139
+ foregroundColor: Colors.black,
140
+ textStyle: const TextStyle(
141
+ fontSize: 18,
142
+ fontWeight: FontWeight.w500,
143
+ letterSpacing: 0,
144
+ ),
145
+ ),
146
+ ),
147
+ segmentedButtonTheme: SegmentedButtonThemeData(
148
+ style: ButtonStyle(
149
+ backgroundColor: WidgetStateProperty.resolveWith((states) {
150
+ if (states.contains(WidgetState.selected)) {
151
+ return lightColorScheme.primary;
152
+ }
153
+ return Colors.grey.shade100;
154
+ }),
155
+ foregroundColor: WidgetStateProperty.resolveWith((states) {
156
+ if (states.contains(WidgetState.selected)) {
157
+ return lightColorScheme.onPrimary;
158
+ }
159
+ return lightColorScheme.onSurface;
160
+ }),
161
+ textStyle: WidgetStatePropertyAll(
162
+ TextStyle(
163
+ fontSize: 16,
164
+ fontWeight: FontWeight.w700,
165
+ color: lightColorScheme.onSurface,
166
+ letterSpacing: 0,
167
+ ),
168
+ ),
169
+ ),
170
+ ),
171
+ toggleButtonsTheme: ToggleButtonsThemeData(
172
+ borderRadius: const BorderRadius.all(Radius.circular(12)),
173
+ borderColor: lightColorScheme.scrim,
174
+ selectedBorderColor: lightColorScheme.scrim,
175
+ selectedColor: lightColorScheme.onPrimary,
176
+ fillColor: lightColorScheme.primary,
177
+ color: lightColorScheme.onSurface,
178
+ constraints: const BoxConstraints(minHeight: 40, minWidth: 140),
179
+ ),
180
+ textTheme: TextTheme(
181
+ bodyMedium: TextStyle(
182
+ fontSize: 16,
183
+ fontWeight: FontWeight.w400,
184
+ wordSpacing: 0,
185
+ letterSpacing: 0,
186
+ color: lightColorScheme.onSurface,
187
+ ),
188
+ bodySmall: TextStyle(
189
+ fontSize: 14,
190
+ fontWeight: FontWeight.w400,
191
+ wordSpacing: 0,
192
+ letterSpacing: 0,
193
+ color: lightColorScheme.onSurface,
194
+ ),
195
+ headlineLarge: TextStyle(
196
+ fontSize: 38,
197
+ fontWeight: FontWeight.w700,
198
+ color: lightColorScheme.secondary,
199
+ ),
200
+ headlineMedium: TextStyle(
201
+ color: lightColorScheme.onSurface,
202
+ fontSize: 24,
203
+ fontWeight: FontWeight.w500,
204
+ letterSpacing: 0.5,
205
+ ),
206
+ labelMedium: TextStyle(
207
+ fontSize: 13,
208
+ fontWeight: FontWeight.w700,
209
+ color: Color(0xFF9A1111),
210
+ letterSpacing: 0,
211
+ ),
212
+ labelLarge: TextStyle(
213
+ fontSize: 16,
214
+ fontWeight: FontWeight.w700,
215
+ color: lightColorScheme.onSurface,
216
+ letterSpacing: 0,
217
+ ),
218
+ ),
219
+ );
220
+
221
+ // Built from its own ThemeData(colorScheme: ...) root rather than
222
+ // light.copyWith(colorScheme: darkColorScheme) — copyWith only overrides
223
+ // the fields listed explicitly here, it never re-derives Material3
224
+ // defaults (scaffoldBackgroundColor, canvasColor, cardColor, dividerColor,
225
+ // ...) from the new colorScheme. Starting from light's already-resolved
226
+ // ThemeData left those pinned to the light scheme's white surface, which
227
+ // is why the Scaffold body stayed white in dark mode.
228
+ static final ThemeData dark =
229
+ ThemeData(
230
+ brightness: Brightness.dark,
231
+ colorScheme: darkColorScheme,
232
+ useMaterial3: true,
233
+ applyElevationOverlayColor: false,
234
+ ).copyWith(
235
+ inputDecorationTheme: InputDecorationTheme(
236
+ focusColor: Colors.transparent,
237
+ hoverColor: Colors.transparent,
238
+ activeIndicatorBorder: BorderSide(color: darkColorScheme.outline),
239
+ hintStyle: const TextStyle(color: Colors.grey),
240
+ ),
241
+ segmentedButtonTheme: SegmentedButtonThemeData(
242
+ style: ButtonStyle(
243
+ backgroundColor: WidgetStateProperty.resolveWith((states) {
244
+ if (states.contains(WidgetState.selected)) {
245
+ return darkColorScheme.primary;
246
+ }
247
+ return darkColorScheme.surfaceContainerHigh;
248
+ }),
249
+ foregroundColor: WidgetStateProperty.resolveWith((states) {
250
+ if (states.contains(WidgetState.selected)) {
251
+ return darkColorScheme.onPrimary;
252
+ }
253
+ return darkColorScheme.onSurface;
254
+ }),
255
+ textStyle: WidgetStatePropertyAll(
256
+ TextStyle(
257
+ fontSize: 16,
258
+ fontWeight: FontWeight.w700,
259
+ color: darkColorScheme.onSurface,
260
+ letterSpacing: 0,
261
+ ),
262
+ ),
263
+ ),
264
+ ),
265
+ toggleButtonsTheme: ToggleButtonsThemeData(
266
+ borderRadius: const BorderRadius.all(Radius.circular(12)),
267
+ borderColor: darkColorScheme.outline,
268
+ selectedBorderColor: darkColorScheme.outline,
269
+ selectedColor: darkColorScheme.onPrimary,
270
+ fillColor: darkColorScheme.primary,
271
+ color: darkColorScheme.onSurface,
272
+ constraints: const BoxConstraints(minHeight: 40, minWidth: 140),
273
+ ),
274
+ appBarTheme: AppBarTheme(
275
+ backgroundColor: darkColorScheme.surface,
276
+ foregroundColor: darkColorScheme.onSurface,
277
+ elevation: 1,
278
+ surfaceTintColor: Colors.transparent,
279
+ ),
280
+ bottomSheetTheme: light.bottomSheetTheme.copyWith(
281
+ backgroundColor: darkColorScheme.surfaceContainerLow,
282
+ shadowColor: Colors.white,
283
+ surfaceTintColor: darkColorScheme.surfaceContainerLow,
284
+ ),
285
+ textSelectionTheme: TextSelectionThemeData(
286
+ cursorColor: Colors.white,
287
+ selectionHandleColor: Colors.white,
288
+ selectionColor: Colors.blueGrey.shade800,
289
+ ),
290
+ dialogTheme: DialogThemeData(
291
+ backgroundColor: darkColorScheme.surface,
292
+ elevation: 1,
293
+ shape: Border(
294
+ top: BorderSide(width: 1.5, color: darkColorScheme.outline),
295
+ ),
296
+ ),
297
+ iconButtonTheme: IconButtonThemeData(
298
+ style: IconButton.styleFrom(
299
+ enableFeedback: true,
300
+ foregroundColor: darkColorScheme.onSurface,
301
+ ),
302
+ ),
303
+ textButtonTheme: TextButtonThemeData(
304
+ style: TextButton.styleFrom(
305
+ enableFeedback: light.textButtonTheme.style!.enableFeedback,
306
+ padding: EdgeInsets.all(0),
307
+ shape: RoundedRectangleBorder(
308
+ borderRadius: BorderRadius.circular(4),
309
+ side: BorderSide(color: darkColorScheme.onSurface, width: 1),
310
+ ),
311
+ textStyle: const TextStyle(
312
+ fontSize: 18,
313
+ fontWeight: FontWeight.w500,
314
+ letterSpacing: 0,
315
+ ),
316
+ elevation: 0,
317
+ shadowColor: Colors.white,
318
+ backgroundColor: darkColorScheme.surfaceContainerLow,
319
+ foregroundColor: darkColorScheme.primary,
320
+ ),
321
+ ),
322
+ textTheme: TextTheme(
323
+ bodyMedium: light.textTheme.bodyMedium!.copyWith(
324
+ color: darkColorScheme.onSurface,
325
+ ),
326
+ bodySmall: light.textTheme.bodyMedium!.copyWith(
327
+ color: darkColorScheme.onSurface,
328
+ ),
329
+ headlineLarge: light.textTheme.headlineLarge!.copyWith(
330
+ color: darkColorScheme.onSurface,
331
+ ),
332
+ headlineMedium: light.textTheme.headlineMedium!.copyWith(
333
+ color: darkColorScheme.onSurface,
334
+ ),
335
+ labelMedium: light.textTheme.labelMedium!.copyWith(
336
+ color: darkColorScheme.error,
337
+ ),
338
+ labelLarge: light.textTheme.labelLarge!.copyWith(
339
+ color: darkColorScheme.onSurface,
340
+ ),
341
+ ),
342
+ );
343
+ }
lib/utils.dart CHANGED
@@ -1,10 +1,10 @@
1
1
  import "package:archive/archive.dart";
2
+ import "package:audioplayers/audioplayers.dart";
2
3
  import "package:flutter/services.dart";
3
4
  import "package:flutter/foundation.dart"
4
5
  show TargetPlatform, compute, defaultTargetPlatform;
5
6
  import "package:flutter/material.dart";
6
7
  import "package:flutter_azure_tts/flutter_azure_tts.dart";
7
- import "package:flutter_soloud/flutter_soloud.dart";
8
8
  import "package:only_bible_app/gen/bible.gen.dart";
9
9
  export "package:only_bible_app/bible_data.dart";
10
10
 
@@ -35,16 +35,25 @@ extension BibleExt on Bible {
35
35
  (int, int) clampBookChapter(Bible bible, int book, int chapter) {
36
36
  final books = bible.books ?? const [];
37
37
  if (books.isEmpty) return (0, 0);
38
+ final clampedBook = book < 0
39
+ ? 0
38
- final clampedBook = book < 0 ? 0 : (book >= books.length ? books.length - 1 : book);
40
+ : (book >= books.length ? books.length - 1 : book);
39
41
  final chapters = books[clampedBook].chapters ?? const [];
40
42
  if (chapters.isEmpty) return (clampedBook, 0);
43
+ final clampedChapter = chapter < 0
44
+ ? 0
41
- final clampedChapter = chapter < 0 ? 0 : (chapter >= chapters.length ? chapters.length - 1 : chapter);
45
+ : (chapter >= chapters.length ? chapters.length - 1 : chapter);
42
46
  return (clampedBook, clampedChapter);
43
47
  }
44
48
 
45
49
  /// Returns the (book, chapter) adjacent to [book]/[chapter], stepping forward
46
50
  /// or backward across book boundaries, or null if already at the very start/end.
47
- (int, int)? adjacentChapter(Bible bible, int book, int chapter, {required bool forward}) {
51
+ (int, int)? adjacentChapter(
52
+ Bible bible,
53
+ int book,
54
+ int chapter, {
55
+ required bool forward,
56
+ }) {
48
57
  final selectedBook = bible.books![book];
49
58
  if (forward) {
50
59
  if (selectedBook.chapters!.length > chapter + 1) {
@@ -132,13 +141,13 @@ Future<Uint8List> convertText(String langCode, String text) async {
132
141
  shortName: langCode,
133
142
  gender: "",
134
143
  locale: langCode,
135
- sampleRateHertz: AudioOutputFormat.raw24khz16BitMonoPcm,
144
+ sampleRateHertz: AudioOutputFormat.audio24khz48kBitrateMonoMp3,
136
145
  voiceType: "",
137
146
  status: "",
138
147
  styles: [],
139
148
  roles: [],
140
149
  ),
141
- audioFormat: AudioOutputFormat.raw24khz16BitMonoPcm,
150
+ audioFormat: AudioOutputFormat.audio24khz48kBitrateMonoMp3,
142
151
  rate: 0.90,
143
152
  text: text,
144
153
  ),
@@ -146,15 +155,30 @@ Future<Uint8List> convertText(String langCode, String text) async {
146
155
  return ttsResponse.audio.buffer.asUint8List();
147
156
  }
148
157
 
158
+ AudioPlayer? _audioPlayer;
159
+
149
160
  bool isAudioPlaybackActive() {
150
- return SoLoud.instance.isInitialized &&
161
+ return _audioPlayer?.state == PlayerState.playing;
151
- SoLoud.instance.getActiveVoiceCount() > 0;
152
162
  }
153
163
 
164
+ bool get isAudioPlaybackInitialized => _audioPlayer != null;
165
+
154
166
  Future<void> ensureAudioPlaybackInitialized() async {
155
- if (!SoLoud.instance.isInitialized) {
156
- await SoLoud.instance.init();
167
+ _audioPlayer ??= AudioPlayer();
157
- }
168
+ }
169
+
170
+ /// Plays [mp3Data] and waits until it finishes, or until [stopAudioPlayback]
171
+ /// cuts it short. No-ops if playback was already stopped.
172
+ Future<void> playAudioClip(Uint8List mp3Data) async {
173
+ final player = _audioPlayer;
174
+ if (player == null) return;
175
+ await player.play(BytesSource(mp3Data, mimeType: "audio/mpeg"));
176
+ await player.onPlayerStateChanged.firstWhere(
177
+ (s) =>
178
+ s == PlayerState.completed ||
179
+ s == PlayerState.stopped ||
180
+ s == PlayerState.disposed,
181
+ );
158
182
  }
159
183
 
160
184
  Future<void>? _audioPlaybackStopFuture;
@@ -174,14 +198,10 @@ Future<void> stopAudioPlayback() {
174
198
  }
175
199
 
176
200
  Future<void> _stopAudioPlayback() async {
201
+ final player = _audioPlayer;
177
- if (!SoLoud.instance.isInitialized) return;
202
+ if (player == null) return;
178
- try {
203
+ _audioPlayer = null;
179
- await SoLoud.instance.disposeAllSources();
204
+ await player.dispose();
180
- } finally {
181
- if (SoLoud.instance.isInitialized) {
182
- SoLoud.instance.deinit();
183
- }
184
- }
185
205
  }
186
206
 
187
207
  Uint8List _decompressBible(Uint8List compressed) {
@@ -190,10 +210,13 @@ Uint8List _decompressBible(Uint8List compressed) {
190
210
 
191
211
  Future<Bible> loadBible(String name) async {
192
212
  final data = await rootBundle.load("assets/bibles/$name.bin.gz");
193
- final decompressed = await compute(_decompressBible, data.buffer.asUint8List());
213
+ final decompressed = await compute(
214
+ _decompressBible,
215
+ data.buffer.asUint8List(),
216
+ );
194
217
  return Bible(decompressed);
195
218
  }
196
219
 
197
- // Error reporting now goes through ErrorReporting.report (package:shared) —
220
+ // Error reporting now goes through ErrorReporting.report (error_reporting.dart) —
198
221
  // see main.dart's ErrorReporting.install call and its use in
199
222
  // actions_state.dart's audio-playback failure handler.
macos/Flutter/Flutter-Debug.xcconfig CHANGED
@@ -1,2 +1 @@
1
- #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2
1
  #include "ephemeral/Flutter-Generated.xcconfig"
macos/Flutter/Flutter-Release.xcconfig CHANGED
@@ -1,2 +1 @@
1
- #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2
1
  #include "ephemeral/Flutter-Generated.xcconfig"
macos/Flutter/GeneratedPluginRegistrant.swift CHANGED
@@ -5,18 +5,16 @@
5
5
  import FlutterMacOS
6
6
  import Foundation
7
7
 
8
+ import audioplayers_darwin
8
9
  import connectivity_plus
9
- import file_picker
10
+ import file_selector_macos
10
- import flutter_secure_storage_darwin
11
11
  import package_info_plus
12
12
  import share_plus
13
- import sqflite_darwin
14
13
 
15
14
  func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
15
+ AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
16
16
  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
17
- FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
17
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
18
- FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
19
18
  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
20
19
  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
21
- SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
22
20
  }
macos/Podfile DELETED
@@ -1,42 +0,0 @@
1
- platform :osx, '10.15'
2
-
3
- # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4
- ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5
-
6
- project 'Runner', {
7
- 'Debug' => :debug,
8
- 'Profile' => :release,
9
- 'Release' => :release,
10
- }
11
-
12
- def flutter_root
13
- generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14
- unless File.exist?(generated_xcode_build_settings_path)
15
- raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16
- end
17
-
18
- File.foreach(generated_xcode_build_settings_path) do |line|
19
- matches = line.match(/FLUTTER_ROOT\=(.*)/)
20
- return matches[1].strip if matches
21
- end
22
- raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23
- end
24
-
25
- require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26
-
27
- flutter_macos_podfile_setup
28
-
29
- target 'Runner' do
30
- use_frameworks!
31
-
32
- flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
33
- target 'RunnerTests' do
34
- inherit! :search_paths
35
- end
36
- end
37
-
38
- post_install do |installer|
39
- installer.pods_project.targets.each do |target|
40
- flutter_additional_macos_build_settings(target)
41
- end
42
- end
macos/Podfile.lock DELETED
@@ -1,22 +0,0 @@
1
- PODS:
2
- - flutter_soloud (0.0.1):
3
- - FlutterMacOS
4
- - FlutterMacOS (1.0.0)
5
-
6
- DEPENDENCIES:
7
- - flutter_soloud (from `Flutter/ephemeral/.symlinks/plugins/flutter_soloud/macos`)
8
- - FlutterMacOS (from `Flutter/ephemeral`)
9
-
10
- EXTERNAL SOURCES:
11
- flutter_soloud:
12
- :path: Flutter/ephemeral/.symlinks/plugins/flutter_soloud/macos
13
- FlutterMacOS:
14
- :path: Flutter/ephemeral
15
-
16
- SPEC CHECKSUMS:
17
- flutter_soloud: 93c20641a100d25bfbc84a8b86e867f350ef29ac
18
- FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
19
-
20
- PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
21
-
22
- COCOAPODS: 1.16.2
macos/Runner.xcodeproj/project.pbxproj CHANGED
@@ -21,7 +21,6 @@
21
21
  /* End PBXAggregateTarget section */
22
22
 
23
23
  /* Begin PBXBuildFile section */
24
- 24975236CA37842BE288C1EF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50275D2F47C1000366FA62E4 /* Pods_RunnerTests.framework */; };
25
24
  331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
26
25
  335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
27
26
  33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
@@ -29,7 +28,6 @@
29
28
  33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
30
29
  33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
31
30
  78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
32
- A5F01B2318AAC442B3788873 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C7846F35E82451F8353334E /* Pods_Runner.framework */; };
33
31
  /* End PBXBuildFile section */
34
32
 
35
33
  /* Begin PBXContainerItemProxy section */
@@ -66,7 +64,6 @@
66
64
  331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
67
65
  331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
68
66
  333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
69
- 334DCAC7AF1120FDF88EE10F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
70
67
  335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
71
68
  33CC10ED2044A3C60003C045 /* only_bible_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = only_bible_app.app; sourceTree = BUILT_PRODUCTS_DIR; };
72
69
  33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -80,16 +77,9 @@
80
77
  33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
81
78
  33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
82
79
  33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
83
- 35B9450ADD66E8BC4C4F3826 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
84
- 35CCE74F1920378D63AB4733 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
85
- 50275D2F47C1000366FA62E4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
86
80
  78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
87
81
  7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
88
- 87E97379FB3EB86D1485488E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
89
82
  9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
90
- 9C7846F35E82451F8353334E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
91
- BFAE72D8B9900A8C04CAB06F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
92
- C6CC86F2F862899310B2073C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
93
83
  /* End PBXFileReference section */
94
84
 
95
85
  /* Begin PBXFrameworksBuildPhase section */
@@ -97,7 +87,6 @@
97
87
  isa = PBXFrameworksBuildPhase;
98
88
  buildActionMask = 2147483647;
99
89
  files = (
100
- 24975236CA37842BE288C1EF /* Pods_RunnerTests.framework in Frameworks */,
101
90
  );
102
91
  runOnlyForDeploymentPostprocessing = 0;
103
92
  };
@@ -106,7 +95,6 @@
106
95
  buildActionMask = 2147483647;
107
96
  files = (
108
97
  78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
109
- A5F01B2318AAC442B3788873 /* Pods_Runner.framework in Frameworks */,
110
98
  );
111
99
  runOnlyForDeploymentPostprocessing = 0;
112
100
  };
@@ -139,8 +127,6 @@
139
127
  33CEB47122A05771004F2AC0 /* Flutter */,
140
128
  331C80D6294CF71000263BE5 /* RunnerTests */,
141
129
  33CC10EE2044A3C60003C045 /* Products */,
142
- D73912EC22F37F3D000D13A0 /* Frameworks */,
143
- 386D91C7345CD19195A06092 /* Pods */,
144
130
  );
145
131
  sourceTree = "<group>";
146
132
  };
@@ -189,29 +175,6 @@
189
175
  path = Runner;
190
176
  sourceTree = "<group>";
191
177
  };
192
- 386D91C7345CD19195A06092 /* Pods */ = {
193
- isa = PBXGroup;
194
- children = (
195
- C6CC86F2F862899310B2073C /* Pods-Runner.debug.xcconfig */,
196
- 35B9450ADD66E8BC4C4F3826 /* Pods-Runner.release.xcconfig */,
197
- 35CCE74F1920378D63AB4733 /* Pods-Runner.profile.xcconfig */,
198
- 334DCAC7AF1120FDF88EE10F /* Pods-RunnerTests.debug.xcconfig */,
199
- 87E97379FB3EB86D1485488E /* Pods-RunnerTests.release.xcconfig */,
200
- BFAE72D8B9900A8C04CAB06F /* Pods-RunnerTests.profile.xcconfig */,
201
- );
202
- name = Pods;
203
- path = Pods;
204
- sourceTree = "<group>";
205
- };
206
- D73912EC22F37F3D000D13A0 /* Frameworks */ = {
207
- isa = PBXGroup;
208
- children = (
209
- 9C7846F35E82451F8353334E /* Pods_Runner.framework */,
210
- 50275D2F47C1000366FA62E4 /* Pods_RunnerTests.framework */,
211
- );
212
- name = Frameworks;
213
- sourceTree = "<group>";
214
- };
215
178
  /* End PBXGroup section */
216
179
 
217
180
  /* Begin PBXNativeTarget section */
@@ -219,7 +182,6 @@
219
182
  isa = PBXNativeTarget;
220
183
  buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
221
184
  buildPhases = (
222
- 109FC7E83F5C961E0F9FC371 /* [CP] Check Pods Manifest.lock */,
223
185
  331C80D1294CF70F00263BE5 /* Sources */,
224
186
  331C80D2294CF70F00263BE5 /* Frameworks */,
225
187
  331C80D3294CF70F00263BE5 /* Resources */,
@@ -238,13 +200,11 @@
238
200
  isa = PBXNativeTarget;
239
201
  buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
240
202
  buildPhases = (
241
- 2FD51143F0581615244DC8B0 /* [CP] Check Pods Manifest.lock */,
242
203
  33CC10E92044A3C60003C045 /* Sources */,
243
204
  33CC10EA2044A3C60003C045 /* Frameworks */,
244
205
  33CC10EB2044A3C60003C045 /* Resources */,
245
206
  33CC110E2044A8840003C045 /* Bundle Framework */,
246
207
  3399D490228B24CF009A79C7 /* ShellScript */,
247
- 86A82F343DF3AEF021A46991 /* [CP] Embed Pods Frameworks */,
248
208
  );
249
209
  buildRules = (
250
210
  );
@@ -333,50 +293,6 @@
333
293
  /* End PBXResourcesBuildPhase section */
334
294
 
335
295
  /* Begin PBXShellScriptBuildPhase section */
336
- 109FC7E83F5C961E0F9FC371 /* [CP] Check Pods Manifest.lock */ = {
337
- isa = PBXShellScriptBuildPhase;
338
- buildActionMask = 2147483647;
339
- files = (
340
- );
341
- inputFileListPaths = (
342
- );
343
- inputPaths = (
344
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
345
- "${PODS_ROOT}/Manifest.lock",
346
- );
347
- name = "[CP] Check Pods Manifest.lock";
348
- outputFileListPaths = (
349
- );
350
- outputPaths = (
351
- "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
352
- );
353
- runOnlyForDeploymentPostprocessing = 0;
354
- shellPath = /bin/sh;
355
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
356
- showEnvVarsInLog = 0;
357
- };
358
- 2FD51143F0581615244DC8B0 /* [CP] Check Pods Manifest.lock */ = {
359
- isa = PBXShellScriptBuildPhase;
360
- buildActionMask = 2147483647;
361
- files = (
362
- );
363
- inputFileListPaths = (
364
- );
365
- inputPaths = (
366
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
367
- "${PODS_ROOT}/Manifest.lock",
368
- );
369
- name = "[CP] Check Pods Manifest.lock";
370
- outputFileListPaths = (
371
- );
372
- outputPaths = (
373
- "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
374
- );
375
- runOnlyForDeploymentPostprocessing = 0;
376
- shellPath = /bin/sh;
377
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
378
- showEnvVarsInLog = 0;
379
- };
380
296
  3399D490228B24CF009A79C7 /* ShellScript */ = {
381
297
  isa = PBXShellScriptBuildPhase;
382
298
  alwaysOutOfDate = 1;
@@ -415,23 +331,6 @@
415
331
  shellPath = /bin/sh;
416
332
  shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
417
333
  };
418
- 86A82F343DF3AEF021A46991 /* [CP] Embed Pods Frameworks */ = {
419
- isa = PBXShellScriptBuildPhase;
420
- buildActionMask = 2147483647;
421
- files = (
422
- );
423
- inputFileListPaths = (
424
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
425
- );
426
- name = "[CP] Embed Pods Frameworks";
427
- outputFileListPaths = (
428
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
429
- );
430
- runOnlyForDeploymentPostprocessing = 0;
431
- shellPath = /bin/sh;
432
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
433
- showEnvVarsInLog = 0;
434
- };
435
334
  /* End PBXShellScriptBuildPhase section */
436
335
 
437
336
  /* Begin PBXSourcesBuildPhase section */
@@ -483,13 +382,12 @@
483
382
  /* Begin XCBuildConfiguration section */
484
383
  331C80DB294CF71000263BE5 /* Debug */ = {
485
384
  isa = XCBuildConfiguration;
486
- baseConfigurationReference = 334DCAC7AF1120FDF88EE10F /* Pods-RunnerTests.debug.xcconfig */;
487
385
  buildSettings = {
488
386
  BUNDLE_LOADER = "$(TEST_HOST)";
489
387
  CURRENT_PROJECT_VERSION = 5;
490
388
  GENERATE_INFOPLIST_FILE = YES;
491
389
  MARKETING_VERSION = 1.0;
492
- PRODUCT_BUNDLE_IDENTIFIER = sh.pyros.only-bible-app.RunnerTests;
390
+ PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
493
391
  PRODUCT_NAME = "$(TARGET_NAME)";
494
392
  SWIFT_VERSION = 5.0;
495
393
  TEST_HOST = "$(BUILT_PRODUCTS_DIR)/only_bible_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/only_bible_app";
@@ -498,13 +396,12 @@
498
396
  };
499
397
  331C80DC294CF71000263BE5 /* Release */ = {
500
398
  isa = XCBuildConfiguration;
501
- baseConfigurationReference = 87E97379FB3EB86D1485488E /* Pods-RunnerTests.release.xcconfig */;
502
399
  buildSettings = {
503
400
  BUNDLE_LOADER = "$(TEST_HOST)";
504
401
  CURRENT_PROJECT_VERSION = 5;
505
402
  GENERATE_INFOPLIST_FILE = YES;
506
403
  MARKETING_VERSION = 1.0;
507
- PRODUCT_BUNDLE_IDENTIFIER = sh.pyros.only-bible-app.RunnerTests;
404
+ PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
508
405
  PRODUCT_NAME = "$(TARGET_NAME)";
509
406
  SWIFT_VERSION = 5.0;
510
407
  TEST_HOST = "$(BUILT_PRODUCTS_DIR)/only_bible_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/only_bible_app";
@@ -513,13 +410,12 @@
513
410
  };
514
411
  331C80DD294CF71000263BE5 /* Profile */ = {
515
412
  isa = XCBuildConfiguration;
516
- baseConfigurationReference = BFAE72D8B9900A8C04CAB06F /* Pods-RunnerTests.profile.xcconfig */;
517
413
  buildSettings = {
518
414
  BUNDLE_LOADER = "$(TEST_HOST)";
519
415
  CURRENT_PROJECT_VERSION = 5;
520
416
  GENERATE_INFOPLIST_FILE = YES;
521
417
  MARKETING_VERSION = 1.0;
522
- PRODUCT_BUNDLE_IDENTIFIER = sh.pyros.only-bible-app.RunnerTests;
418
+ PRODUCT_BUNDLE_IDENTIFIER = "sh.pyros.only-bible-app.RunnerTests";
523
419
  PRODUCT_NAME = "$(TARGET_NAME)";
524
420
  SWIFT_VERSION = 5.0;
525
421
  TEST_HOST = "$(BUILT_PRODUCTS_DIR)/only_bible_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/only_bible_app";
macos/Runner.xcworkspace/contents.xcworkspacedata CHANGED
@@ -4,7 +4,4 @@
4
4
  <FileRef
5
5
  location = "group:Runner.xcodeproj">
6
6
  </FileRef>
7
- <FileRef
8
- location = "group:Pods/Pods.xcodeproj">
9
- </FileRef>
10
7
  </Workspace>
macos/Runner/Info.plist CHANGED
@@ -32,9 +32,5 @@
32
32
  <string>MainMenu</string>
33
33
  <key>NSPrincipalClass</key>
34
34
  <string>NSApplication</string>
35
- <key>NSPhotoLibraryUsageDescription</key>
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>NSAppleMusicUsageDescription</key>
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>
39
35
  </dict>
40
36
  </plist>
macos/fastlane/Fastfile CHANGED
@@ -60,15 +60,16 @@ platform :mac do
60
60
  end
61
61
 
62
62
  desc "Push a new release build to the Mac App Store"
63
- lane :release do
63
+ lane :release do |options|
64
- screenshots
64
+ screenshots unless options[:skip_screenshots]
65
65
  build
66
66
  api_key = api_key()
67
67
  upload_to_app_store(
68
68
  api_key: api_key,
69
69
  platform: "osx",
70
70
  pkg: lane_context[SharedValues::PKG_OUTPUT_PATH],
71
- overwrite_screenshots: true
71
+ overwrite_screenshots: true,
72
+ precheck_include_in_app_purchases: false
72
73
  )
73
74
  end
74
75
 
macos/fastlane/report.xml CHANGED
@@ -5,32 +5,67 @@
5
5
 
6
6
 
7
7
 
8
- <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000162">
8
+ <testcase classname="fastlane.lanes" name="00: default_platform" time="0.000187">
9
9
 
10
10
  </testcase>
11
11
 
12
12
 
13
- <testcase classname="fastlane.lanes" name="1: Switch to mac api_key lane" time="5.4e-05">
13
+ <testcase classname="fastlane.lanes" name="01: Switch to mac build lane" time="0.000122">
14
14
 
15
15
  </testcase>
16
16
 
17
17
 
18
- <testcase classname="fastlane.lanes" name="2: app_store_connect_api_key" time="0.001757">
18
+ <testcase classname="fastlane.lanes" name="02: Switch to mac certs lane" time="0.000138">
19
19
 
20
20
  </testcase>
21
21
 
22
22
 
23
- <testcase classname="fastlane.lanes" name="3: cert" time="1.717703">
23
+ <testcase classname="fastlane.lanes" name="03: Switch to mac api_key lane" time="7.1e-05">
24
24
 
25
25
  </testcase>
26
26
 
27
27
 
28
- <testcase classname="fastlane.lanes" name="4: sigh" time="1.570648">
28
+ <testcase classname="fastlane.lanes" name="04: app_store_connect_api_key" time="0.003265">
29
29
 
30
30
  </testcase>
31
31
 
32
32
 
33
- <testcase classname="fastlane.lanes" name="5: cert" time="1.648573">
33
+ <testcase classname="fastlane.lanes" name="05: cert" time="1.472711">
34
+
35
+ </testcase>
36
+
37
+
38
+ <testcase classname="fastlane.lanes" name="06: sigh" time="1.169831">
39
+
40
+ </testcase>
41
+
42
+
43
+ <testcase classname="fastlane.lanes" name="07: cert" time="1.869459">
44
+
45
+ </testcase>
46
+
47
+
48
+ <testcase classname="fastlane.lanes" name="08: cd ../.. &amp;&amp; flutter build macos --config-only" time="7.146192">
49
+
50
+ </testcase>
51
+
52
+
53
+ <testcase classname="fastlane.lanes" name="09: build_mac_app" time="86.815746">
54
+
55
+ </testcase>
56
+
57
+
58
+ <testcase classname="fastlane.lanes" name="10: Switch to mac api_key lane" time="0.000101">
59
+
60
+ </testcase>
61
+
62
+
63
+ <testcase classname="fastlane.lanes" name="11: app_store_connect_api_key" time="0.00169">
64
+
65
+ </testcase>
66
+
67
+
68
+ <testcase classname="fastlane.lanes" name="12: upload_to_app_store" time="481.245784">
34
69
 
35
70
  </testcase>
36
71
 
pubspec.lock CHANGED
@@ -65,22 +65,62 @@ packages:
65
65
  url: "https://pub.dev"
66
66
  source: hosted
67
67
  version: "1.5.0"
68
+ audioplayers:
69
+ dependency: "direct main"
68
- aws_common:
70
+ description:
71
+ name: audioplayers
72
+ sha256: "2ba4bb2944baacbdd5372ff8254a8e7feb8c10d7739545e392f5605a8f618745"
73
+ url: "https://pub.dev"
74
+ source: hosted
75
+ version: "6.8.1"
76
+ audioplayers_android:
77
+ dependency: transitive
78
+ description:
79
+ name: audioplayers_android
80
+ sha256: f5ff5b15620fbab8cb0849e9636c48e2b96c3f0f71723bbbe2ad3c761b205f05
81
+ url: "https://pub.dev"
82
+ source: hosted
83
+ version: "5.3.0"
84
+ audioplayers_darwin:
85
+ dependency: transitive
86
+ description:
87
+ name: audioplayers_darwin
88
+ sha256: "1ca553add991384ecf421b9569da850f3ab2472ffb83f6970b0416365abc51be"
89
+ url: "https://pub.dev"
90
+ source: hosted
91
+ version: "6.5.0"
92
+ audioplayers_linux:
93
+ dependency: transitive
94
+ description:
95
+ name: audioplayers_linux
96
+ sha256: "15178b726b7cdee5364d0463c8d445630c4e0fb7d26612b73c767e7d25de9417"
97
+ url: "https://pub.dev"
98
+ source: hosted
99
+ version: "4.3.0"
100
+ audioplayers_platform_interface:
69
101
  dependency: transitive
70
102
  description:
71
- name: aws_common
103
+ name: audioplayers_platform_interface
72
- sha256: "939b46121d41c6d6d1419a6f242baefa2403b67ab04919465022cff297b86763"
104
+ sha256: "765f6f0e6dca55cb471c9483fc77700564b3484d19198aca4ebb5147c6c85acb"
73
105
  url: "https://pub.dev"
74
106
  source: hosted
75
- version: "0.7.15"
107
+ version: "7.2.0"
76
- aws_signature_v4:
108
+ audioplayers_web:
77
109
  dependency: transitive
78
110
  description:
79
- name: aws_signature_v4
111
+ name: audioplayers_web
80
- sha256: "83d3fd9402f2a4145ec96b972da779d8658f9505cdd52888d6cbcc2551f5ad88"
112
+ sha256: ae1e0103c865a03e273f6d13d97b93f5595eac09915729cd5e37ef96e2857319
81
113
  url: "https://pub.dev"
82
114
  source: hosted
115
+ version: "5.3.0"
116
+ audioplayers_windows:
117
+ dependency: transitive
118
+ description:
119
+ name: audioplayers_windows
120
+ sha256: a70ae82bba2dfcb6eb03dd4815d737a2d46d33ea5a96a03f535cfcaac490e413
121
+ url: "https://pub.dev"
122
+ source: hosted
83
- version: "0.6.13"
123
+ version: "4.4.1"
84
124
  boolean_selector:
85
125
  dependency: transitive
86
126
  description:
@@ -353,14 +393,70 @@ packages:
353
393
  url: "https://pub.dev"
354
394
  source: hosted
355
395
  version: "7.0.1"
356
- file_picker:
396
+ file_selector:
357
397
  dependency: "direct main"
358
398
  description:
359
- name: file_picker
399
+ name: file_selector
400
+ sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a
401
+ url: "https://pub.dev"
402
+ source: hosted
403
+ version: "1.1.0"
404
+ file_selector_android:
405
+ dependency: transitive
406
+ description:
407
+ name: file_selector_android
408
+ sha256: "1d45e9910f68c16eb0c74f0b10097ad81aed516ea28054c027137e8f7d75e840"
409
+ url: "https://pub.dev"
410
+ source: hosted
411
+ version: "0.5.2+9"
412
+ file_selector_ios:
413
+ dependency: transitive
414
+ description:
415
+ name: file_selector_ios
416
+ sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca
417
+ url: "https://pub.dev"
418
+ source: hosted
419
+ version: "0.5.3+5"
420
+ file_selector_linux:
421
+ dependency: transitive
422
+ description:
423
+ name: file_selector_linux
424
+ sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
425
+ url: "https://pub.dev"
426
+ source: hosted
427
+ version: "0.9.4"
428
+ file_selector_macos:
429
+ dependency: transitive
430
+ description:
431
+ name: file_selector_macos
432
+ sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
433
+ url: "https://pub.dev"
434
+ source: hosted
435
+ version: "0.9.5"
436
+ file_selector_platform_interface:
437
+ dependency: transitive
438
+ description:
439
+ name: file_selector_platform_interface
440
+ sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
441
+ url: "https://pub.dev"
442
+ source: hosted
443
+ version: "2.7.0"
444
+ file_selector_web:
445
+ dependency: transitive
446
+ description:
447
+ name: file_selector_web
360
- sha256: "29cc1fdb20613876cc7afc529738c1c0f11a9ca159b010edad0c566ac330847e"
448
+ sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3"
449
+ url: "https://pub.dev"
450
+ source: hosted
451
+ version: "0.9.5"
452
+ file_selector_windows:
453
+ dependency: transitive
454
+ description:
455
+ name: file_selector_windows
456
+ sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
361
457
  url: "https://pub.dev"
362
458
  source: hosted
363
- version: "11.0.3"
459
+ version: "0.9.3+5"
364
460
  fixnum:
365
461
  dependency: transitive
366
462
  description:
@@ -390,14 +486,6 @@ packages:
390
486
  url: "https://pub.dev"
391
487
  source: hosted
392
488
  version: "1.0.0"
393
- flutter_cache_manager:
394
- dependency: transitive
395
- description:
396
- name: flutter_cache_manager
397
- sha256: "1de7849213b4c73c85aca7e0ac687a9a5d82ccdb594366b9dcc26cb6a2189cd2"
398
- url: "https://pub.dev"
399
- source: hosted
400
- version: "3.4.2"
401
489
  flutter_driver:
402
490
  dependency: transitive
403
491
  description: flutter
@@ -443,70 +531,6 @@ packages:
443
531
  url: "https://pub.dev"
444
532
  source: hosted
445
533
  version: "2.4.8"
446
- flutter_plugin_android_lifecycle:
447
- dependency: transitive
448
- description:
449
- name: flutter_plugin_android_lifecycle
450
- sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
451
- url: "https://pub.dev"
452
- source: hosted
453
- version: "2.0.35"
454
- flutter_secure_storage:
455
- dependency: transitive
456
- description:
457
- name: flutter_secure_storage
458
- sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
459
- url: "https://pub.dev"
460
- source: hosted
461
- version: "10.3.1"
462
- flutter_secure_storage_darwin:
463
- dependency: transitive
464
- description:
465
- name: flutter_secure_storage_darwin
466
- sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
467
- url: "https://pub.dev"
468
- source: hosted
469
- version: "0.3.2"
470
- flutter_secure_storage_linux:
471
- dependency: transitive
472
- description:
473
- name: flutter_secure_storage_linux
474
- sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
475
- url: "https://pub.dev"
476
- source: hosted
477
- version: "3.0.1"
478
- flutter_secure_storage_platform_interface:
479
- dependency: transitive
480
- description:
481
- name: flutter_secure_storage_platform_interface
482
- sha256: "06686df417f34fe9f963ed217b7fdce250e2f637ddd6c374d7eb054549770633"
483
- url: "https://pub.dev"
484
- source: hosted
485
- version: "2.0.2"
486
- flutter_secure_storage_web:
487
- dependency: transitive
488
- description:
489
- name: flutter_secure_storage_web
490
- sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
491
- url: "https://pub.dev"
492
- source: hosted
493
- version: "2.1.1"
494
- flutter_secure_storage_windows:
495
- dependency: transitive
496
- description:
497
- name: flutter_secure_storage_windows
498
- sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
499
- url: "https://pub.dev"
500
- source: hosted
501
- version: "4.1.0"
502
- flutter_soloud:
503
- dependency: "direct main"
504
- description:
505
- name: flutter_soloud
506
- sha256: d275ff1fcaca64eab81189f260613b42787655d213013ab316ecd31bb52a5913
507
- url: "https://pub.dev"
508
- source: hosted
509
- version: "3.5.4"
510
534
  flutter_test:
511
535
  dependency: "direct dev"
512
536
  description: flutter
@@ -594,14 +618,6 @@ packages:
594
618
  url: "https://pub.dev"
595
619
  source: hosted
596
620
  version: "1.6.0"
597
- http2:
598
- dependency: transitive
599
- description:
600
- name: http2
601
- sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa"
602
- url: "https://pub.dev"
603
- source: hosted
604
- version: "2.3.1"
605
621
  http_multi_server:
606
622
  dependency: transitive
607
623
  description:
@@ -783,14 +799,6 @@ packages:
783
799
  url: "https://pub.dev"
784
800
  source: hosted
785
801
  version: "9.5.0"
786
- os_detect:
787
- dependency: transitive
788
- description:
789
- name: os_detect
790
- sha256: "7d87c0dd98c6faf110d5aa498e9a6df02ffce4bb78cc9cfc8ad02929be9bb71f"
791
- url: "https://pub.dev"
792
- source: hosted
793
- version: "2.0.3"
794
802
  package_config:
795
803
  dependency: transitive
796
804
  description:
@@ -959,14 +967,6 @@ packages:
959
967
  url: "https://pub.dev"
960
968
  source: hosted
961
969
  version: "0.6.0"
962
- rxdart:
963
- dependency: transitive
964
- description:
965
- name: rxdart
966
- sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
967
- url: "https://pub.dev"
968
- source: hosted
969
- version: "0.28.0"
970
970
  serverpod_serialization:
971
971
  dependency: transitive
972
972
  description:
@@ -991,13 +991,6 @@ packages:
991
991
  url: "https://pub.dev"
992
992
  source: hosted
993
993
  version: "6.1.0"
994
- shared:
995
- dependency: "direct main"
996
- description:
997
- path: "../r2-backed-apps/packages/shared"
998
- relative: true
999
- source: path
1000
- version: "0.0.1"
1001
994
  shelf:
1002
995
  dependency: transitive
1003
996
  description:
@@ -1075,46 +1068,6 @@ packages:
1075
1068
  url: "https://pub.dev"
1076
1069
  source: hosted
1077
1070
  version: "7.0.0"
1078
- sqflite:
1079
- dependency: transitive
1080
- description:
1081
- name: sqflite
1082
- sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
1083
- url: "https://pub.dev"
1084
- source: hosted
1085
- version: "2.4.3"
1086
- sqflite_android:
1087
- dependency: transitive
1088
- description:
1089
- name: sqflite_android
1090
- sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
1091
- url: "https://pub.dev"
1092
- source: hosted
1093
- version: "2.4.3"
1094
- sqflite_common:
1095
- dependency: transitive
1096
- description:
1097
- name: sqflite_common
1098
- sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
1099
- url: "https://pub.dev"
1100
- source: hosted
1101
- version: "2.5.11"
1102
- sqflite_darwin:
1103
- dependency: transitive
1104
- description:
1105
- name: sqflite_darwin
1106
- sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
1107
- url: "https://pub.dev"
1108
- source: hosted
1109
- version: "2.4.3+1"
1110
- sqflite_platform_interface:
1111
- dependency: transitive
1112
- description:
1113
- name: sqflite_platform_interface
1114
- sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
1115
- url: "https://pub.dev"
1116
- source: hosted
1117
- version: "2.4.1"
1118
1071
  stack_trace:
1119
1072
  dependency: transitive
1120
1073
  description:
pubspec.yaml CHANGED
@@ -1,7 +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.1+20
4
+ version: 3.7.0+23
5
5
 
6
6
  environment:
7
7
  sdk: '^3.12.2'
@@ -18,13 +18,11 @@ dependencies:
18
18
  async_redux: ^28.0.0
19
19
  flat_buffers: ^25.9.23
20
20
  go_router: ^17.1.0
21
- flutter_soloud: ^3.5.4
22
21
  archive: ^4.0.9
23
22
  envied: ^1.3.8
24
- file_picker: ^11.0.2
25
23
  flutter_azure_tts: ^1.0.0
26
- shared:
24
+ audioplayers: ^6.8.1
27
- path: ../r2-backed-apps/packages/shared
25
+ file_selector: ^1.1.0
28
26
 
29
27
  dev_dependencies:
30
28
  flutter_test:
readme.md CHANGED
@@ -67,60 +67,18 @@ signing details; iOS/macOS certs and provisioning profiles are fetched automatic
67
67
  For iOS/macOS, `overwrite_screenshots: true` is set so App Store Connect's existing
68
68
  screenshots are always replaced rather than skipped as duplicates.
69
69
 
70
- ### android
70
+ ### All platforms at once
71
71
 
72
72
  ```
73
- cd android; fastlane deploy
73
+ make release
74
74
  ```
75
75
 
76
- `fastlane deploy` regenerates screenshots, builds the appbundle and uploads to the Play Store in one step.
76
+ Regenerates screenshots once, then builds, uploads and submits the appbundle to
77
- There is no separate `release` lane for android `deploy` is the one to use.
77
+ the Play Store, the ipa to the App Store, and the pkg to the Mac App Store,
78
+ one after another. Equivalent to running `make release-android`,
79
+ `make release-ios` and `make release-macos` in sequence — use those
80
+ individually to release just one platform without the others.
78
81
 
79
- To upload an appbundle you've already built instead:
80
-
81
- ```
82
- flutter build appbundle --release
83
- cd android; fastlane upload
84
- ```
85
-
86
- ### iOS
87
-
88
- Certificates and the App Store provisioning profile are created/reused automatically via `fastlane certs` (runs `cert` + `sigh` against App Store Connect), so no manual Xcode Signing tab setup is needed.
89
-
90
- ```
91
- cd ios; fastlane release
92
- ```
93
-
94
- `fastlane release` regenerates screenshots, ensures signing certs/provisioning profile, builds and submits to App Store Connect in one step.
95
-
96
- To upload an ipa you've already built instead:
97
-
98
- ```
99
- flutter build ipa --release
100
- cd ios; fastlane upload
101
- ```
102
-
103
- ### macOS
104
-
105
- Certificates and the App Store provisioning profile are created/reused automatically via `fastlane certs` (runs `cert` + `sigh` against App Store Connect), so no manual Xcode Signing tab setup is needed.
106
-
107
- ```
108
- cd macos; fastlane release
109
- ```
110
-
111
- `fastlane release` regenerates screenshots, ensures signing certs/provisioning profile, builds, then uploads to App Store Connect and submits for review.
112
-
113
- To build a signed `only-bible-app.pkg` without uploading it:
114
-
115
- ```
116
- cd macos; fastlane build
117
- ```
118
-
119
- To upload a package you've already built (via `fastlane build` or Xcode) instead:
120
-
121
- ```
122
- cd macos; fastlane upload
123
- ```
124
82
 
125
83
  # Data Backup
126
84
 
test/app_logic_test.dart CHANGED
@@ -3,6 +3,7 @@ import "dart:io";
3
3
  import "package:async_redux/async_redux.dart";
4
4
  import "package:flutter/material.dart";
5
5
  import "package:flutter_test/flutter_test.dart";
6
+ import "package:go_router/go_router.dart";
6
7
  import "package:path_provider_platform_interface/path_provider_platform_interface.dart";
7
8
  import "package:only_bible_app/env.dart";
8
9
  import "package:only_bible_app/gen/bible.gen.dart";
@@ -10,7 +11,7 @@ import "package:only_bible_app/store/actions_navigation.dart";
10
11
  import "package:only_bible_app/store/actions_state.dart";
11
12
  import "package:only_bible_app/store/app_persistor.dart";
12
13
  import "package:only_bible_app/store/app_state.dart";
13
- import "package:shared/shared.dart";
14
+ import "package:only_bible_app/theme.dart";
14
15
  import "package:only_bible_app/utils.dart";
15
16
 
16
17
  /// A tiny 3-book Bible used to exercise book/chapter boundary logic without
@@ -37,15 +38,30 @@ Bible buildTestBible() {
37
38
  index: 0,
38
39
  book: 0,
39
40
  verses: [
41
+ VerseObjectBuilder(
42
+ index: 0,
43
+ book: 0,
44
+ chapter: 0,
40
- VerseObjectBuilder(index: 0, book: 0, chapter: 0, text: "In the beginning..."),
45
+ text: "In the beginning...",
46
+ ),
47
+ VerseObjectBuilder(
48
+ index: 1,
49
+ book: 0,
50
+ chapter: 0,
41
- VerseObjectBuilder(index: 1, book: 0, chapter: 0, text: "And the earth..."),
51
+ text: "And the earth...",
52
+ ),
42
53
  ],
43
54
  ),
44
55
  ChapterObjectBuilder(
45
56
  index: 1,
46
57
  book: 0,
47
58
  verses: [
59
+ VerseObjectBuilder(
60
+ index: 0,
61
+ book: 0,
62
+ chapter: 1,
48
- VerseObjectBuilder(index: 0, book: 0, chapter: 1, text: "Chapter two verse one"),
63
+ text: "Chapter two verse one",
64
+ ),
49
65
  ],
50
66
  ),
51
67
  ],
@@ -58,7 +74,12 @@ Bible buildTestBible() {
58
74
  index: 0,
59
75
  book: 1,
60
76
  verses: [
77
+ VerseObjectBuilder(
78
+ index: 0,
79
+ book: 1,
80
+ chapter: 0,
61
- VerseObjectBuilder(index: 0, book: 1, chapter: 0, text: "Judges chapter one verse one"),
81
+ text: "Judges chapter one verse one",
82
+ ),
62
83
  ],
63
84
  ),
64
85
  ],
@@ -70,12 +91,26 @@ Bible buildTestBible() {
70
91
  ChapterObjectBuilder(
71
92
  index: 0,
72
93
  book: 2,
94
+ verses: [
95
+ VerseObjectBuilder(
96
+ index: 0,
97
+ book: 2,
98
+ chapter: 0,
73
- verses: [VerseObjectBuilder(index: 0, book: 2, chapter: 0, text: "Rev 1:1")],
99
+ text: "Rev 1:1",
100
+ ),
101
+ ],
74
102
  ),
75
103
  ChapterObjectBuilder(
76
104
  index: 1,
77
105
  book: 2,
106
+ verses: [
107
+ VerseObjectBuilder(
108
+ index: 0,
109
+ book: 2,
110
+ chapter: 1,
78
- verses: [VerseObjectBuilder(index: 0, book: 2, chapter: 1, text: "Rev 2:1")],
111
+ text: "Rev 2:1",
112
+ ),
113
+ ],
79
114
  ),
80
115
  ],
81
116
  ),
@@ -84,6 +119,20 @@ Bible buildTestBible() {
84
119
  return Bible(builder.toBytes());
85
120
  }
86
121
 
122
+ /// A minimal router matching app.dart's real "/chapter/:bookIndex/:chapterIndex"
123
+ /// route shape, for exercising navigation actions that call `router.push`/`pop`.
124
+ GoRouter buildTestRouter() {
125
+ return GoRouter(
126
+ initialLocation: "/chapter/0/0",
127
+ routes: [
128
+ GoRoute(
129
+ path: "/chapter/:bookIndex/:chapterIndex",
130
+ builder: (context, state) => const SizedBox(),
131
+ ),
132
+ ],
133
+ );
134
+ }
135
+
87
136
  class _FakePathProviderPlatform extends PathProviderPlatform {
88
137
  _FakePathProviderPlatform(this.path);
89
138
  final String path;
@@ -124,26 +173,35 @@ void main() {
124
173
  });
125
174
 
126
175
  group("safe bible accessors", () {
176
+ test(
127
- test("bookAt returns null instead of throwing for an out-of-range index", () {
177
+ "bookAt returns null instead of throwing for an out-of-range index",
178
+ () {
128
- final bible = buildTestBible();
179
+ final bible = buildTestBible();
129
- expect(bible.bookAt(-1), isNull);
180
+ expect(bible.bookAt(-1), isNull);
130
- expect(bible.bookAt(99), isNull);
181
+ expect(bible.bookAt(99), isNull);
131
- expect(bible.bookAt(1)?.name, "Judges");
182
+ expect(bible.bookAt(1)?.name, "Judges");
183
+ },
132
- });
184
+ );
185
+
133
-
186
+ test(
134
- test("chapterAt returns null instead of throwing for an out-of-range index", () {
187
+ "chapterAt returns null instead of throwing for an out-of-range index",
188
+ () {
135
- final genesis = buildTestBible().bookAt(0)!;
189
+ final genesis = buildTestBible().bookAt(0)!;
136
- expect(genesis.chapterAt(-1), isNull);
190
+ expect(genesis.chapterAt(-1), isNull);
137
- expect(genesis.chapterAt(99), isNull);
191
+ expect(genesis.chapterAt(99), isNull);
138
- expect(genesis.chapterAt(1)?.index, 1);
192
+ expect(genesis.chapterAt(1)?.index, 1);
193
+ },
139
- });
194
+ );
195
+
140
-
196
+ test(
141
- test("verseAt returns null instead of throwing for an out-of-range index", () {
197
+ "verseAt returns null instead of throwing for an out-of-range index",
198
+ () {
142
- final chapter = buildTestBible().bookAt(0)!.chapterAt(0)!;
199
+ final chapter = buildTestBible().bookAt(0)!.chapterAt(0)!;
143
- expect(chapter.verseAt(-1), isNull);
200
+ expect(chapter.verseAt(-1), isNull);
144
- expect(chapter.verseAt(99), isNull);
201
+ expect(chapter.verseAt(99), isNull);
145
- expect(chapter.verseAt(1)?.text, "And the earth...");
202
+ expect(chapter.verseAt(1)?.text, "And the earth...");
203
+ },
146
- });
204
+ );
147
205
  });
148
206
 
149
207
  group("Book.shortName", () {
@@ -155,13 +213,19 @@ void main() {
155
213
  test("disambiguates Judges vs Jude", () {
156
214
  expect(anyBook.shortName("Judges"), "Jdg");
157
215
  expect(anyBook.shortName("Jude"), "Jud");
216
+ expect(
217
+ anyBook.shortName("Judges"),
158
- expect(anyBook.shortName("Judges"), isNot(equals(anyBook.shortName("Jude"))));
218
+ isNot(equals(anyBook.shortName("Jude"))),
219
+ );
159
220
  });
160
221
 
161
222
  test("disambiguates Philemon vs Philippians", () {
162
223
  expect(anyBook.shortName("Philemon"), "Phm");
163
224
  expect(anyBook.shortName("Philippians"), "Php");
225
+ expect(
226
+ anyBook.shortName("Philemon"),
164
- expect(anyBook.shortName("Philemon"), isNot(equals(anyBook.shortName("Philippians"))));
227
+ isNot(equals(anyBook.shortName("Philippians"))),
228
+ );
165
229
  });
166
230
 
167
231
  test("does not throw for 3-letter book names", () {
@@ -172,7 +236,10 @@ void main() {
172
236
  test("keeps numbered-book abbreviations distinct", () {
173
237
  expect(anyBook.shortName("1 Samuel"), "1Sa");
174
238
  expect(anyBook.shortName("2 Samuel"), "2Sa");
239
+ expect(
240
+ anyBook.shortName("1 Timothy"),
175
- expect(anyBook.shortName("1 Timothy"), isNot(equals(anyBook.shortName("1 Thessalonians"))));
241
+ isNot(equals(anyBook.shortName("1 Thessalonians"))),
242
+ );
176
243
  });
177
244
  });
178
245
 
@@ -186,7 +253,13 @@ void main() {
186
253
  fontSize: 20,
187
254
  themeMode: ThemeMode.dark,
188
255
  highlightHistory: [
256
+ HighlightHistoryEntry(
257
+ book: 0,
258
+ chapter: 0,
259
+ verseIndex: 0,
260
+ colorIndex: 1,
189
- HighlightHistoryEntry(book: 0, chapter: 0, verseIndex: 0, colorIndex: 1, timestamp: DateTime.utc(2026, 1, 1)),
261
+ timestamp: DateTime.utc(2026, 1, 1),
262
+ ),
190
263
  ],
191
264
  );
192
265
  final restored = AppState.fromJson(state.toJson(), bible);
@@ -197,12 +270,18 @@ void main() {
197
270
  expect(restored.highlightHistory.single.book, 0);
198
271
  });
199
272
 
273
+ test(
200
- test("clamps an out-of-range persisted savedBook/savedChapter instead of crashing", () {
274
+ "clamps an out-of-range persisted savedBook/savedChapter instead of crashing",
275
+ () {
201
- final bible = buildTestBible();
276
+ final bible = buildTestBible();
202
- final state = AppState.fromJson({"savedBook": 999, "savedChapter": 999}, bible);
277
+ final state = AppState.fromJson({
278
+ "savedBook": 999,
279
+ "savedChapter": 999,
280
+ }, bible);
203
- expect(state.savedBook, 2);
281
+ expect(state.savedBook, 2);
204
- expect(state.savedChapter, 1);
282
+ expect(state.savedChapter, 1);
283
+ },
205
- });
284
+ );
206
285
 
207
286
  test("defaults missing fields", () {
208
287
  final bible = buildTestBible();
@@ -215,21 +294,24 @@ void main() {
215
294
  });
216
295
 
217
296
  group("SelectVerseAction", () {
297
+ test(
218
- test("deselecting a verse only removes the matching book+chapter+index", () async {
298
+ "deselecting a verse only removes the matching book+chapter+index",
299
+ () async {
219
- final bible = buildTestBible();
300
+ final bible = buildTestBible();
220
- // Same verse index (0) in two different chapters of the same book -
301
+ // Same verse index (0) in two different chapters of the same book -
221
- // exactly the collision the deselect-path bug missed.
302
+ // exactly the collision the deselect-path bug missed.
222
- final verseInChapter0 = bible.bookAt(0)!.chapterAt(0)!.verseAt(0)!;
303
+ final verseInChapter0 = bible.bookAt(0)!.chapterAt(0)!.verseAt(0)!;
223
- final verseInChapter1 = bible.bookAt(0)!.chapterAt(1)!.verseAt(0)!;
304
+ final verseInChapter1 = bible.bookAt(0)!.chapterAt(1)!.verseAt(0)!;
224
-
305
+
225
- final store = Store<AppState>(initialState: AppState(bible: bible));
306
+ final store = Store<AppState>(initialState: AppState(bible: bible));
226
- await store.dispatchAndWait(SelectVerseAction(verseInChapter0));
307
+ await store.dispatchAndWait(SelectVerseAction(verseInChapter0));
227
- await store.dispatchAndWait(SelectVerseAction(verseInChapter1));
308
+ await store.dispatchAndWait(SelectVerseAction(verseInChapter1));
228
- expect(store.state.selectedVerses.length, 2);
309
+ expect(store.state.selectedVerses.length, 2);
229
-
310
+
230
- await store.dispatchAndWait(SelectVerseAction(verseInChapter0));
311
+ await store.dispatchAndWait(SelectVerseAction(verseInChapter0));
231
- expect(store.state.selectedVerses, [verseInChapter1]);
312
+ expect(store.state.selectedVerses, [verseInChapter1]);
313
+ },
232
- });
314
+ );
233
315
  });
234
316
 
235
317
  group("SetHighlightAction", () {
@@ -238,9 +320,17 @@ void main() {
238
320
  final verse = bible.bookAt(0)!.chapterAt(0)!.verseAt(0)!;
239
321
  final existingHistory = List.generate(
240
322
  500,
323
+ (i) => HighlightHistoryEntry(
324
+ book: 0,
325
+ chapter: 0,
326
+ verseIndex: 1,
327
+ colorIndex: 0,
328
+ timestamp: DateTime.utc(2026, 1, 1),
329
+ ),
330
+ );
331
+ final store = Store<AppState>(
241
- (i) => HighlightHistoryEntry(book: 0, chapter: 0, verseIndex: 1, colorIndex: 0, timestamp: DateTime.utc(2026, 1, 1)),
332
+ initialState: AppState(bible: bible, highlightHistory: existingHistory),
242
333
  );
243
- final store = Store<AppState>(initialState: AppState(bible: bible, highlightHistory: existingHistory));
244
334
  await store.dispatchAndWait(SetHighlightAction([verse], 2));
245
335
  expect(store.state.highlightHistory.length, 500);
246
336
  expect(store.state.highlights["0:0:0"], 2);
@@ -252,7 +342,11 @@ void main() {
252
342
  final bible = buildTestBible();
253
343
  final verse = bible.bookAt(0)!.chapterAt(0)!.verseAt(0)!;
254
344
  final store = Store<AppState>(
345
+ initialState: AppState(
346
+ bible: bible,
255
- initialState: AppState(bible: bible, highlights: {"0:0:0": 1}, selectedVerses: [verse]),
347
+ highlights: {"0:0:0": 1},
348
+ selectedVerses: [verse],
349
+ ),
256
350
  );
257
351
  await store.dispatchAndWait(RemoveHighlightAction([verse]));
258
352
  expect(store.state.highlights.containsKey("0:0:0"), isFalse);
@@ -260,15 +354,6 @@ void main() {
260
354
  });
261
355
  });
262
356
 
263
- // NextChapterAction/PreviousChapterAction/GoToChapterAction all call
264
- // stopAudioPlayback(), which touches flutter_soloud's FFI bindings. Those
265
- // bindings dlsym a native symbol that is only present once the plugin's
266
- // native library is loaded by a real app process — never true in this
267
- // headless `flutter test` VM — so dispatching those actions directly here
268
- // always throws regardless of the app logic under test. Instead we test
269
- // the pure decision logic each action delegates to (adjacentChapter) and
270
- // the plain sync action (UpdateChapterAction) that the ordering fix in
271
- // GoToChapterAction relies on.
272
357
  group("adjacentChapter", () {
273
358
  test("moves to the next chapter within the same book", () {
274
359
  final bible = buildTestBible();
@@ -304,7 +389,9 @@ void main() {
304
389
  group("UpdateChapterAction", () {
305
390
  test("updates savedBook/savedChapter", () async {
306
391
  final bible = buildTestBible();
392
+ final store = Store<AppState>(
307
- final store = Store<AppState>(initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0));
393
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
394
+ );
308
395
  await store.dispatchAndWait(UpdateChapterAction(1, 0));
309
396
  expect(store.state.savedBook, 1);
310
397
  expect(store.state.savedChapter, 0);
@@ -314,7 +401,9 @@ void main() {
314
401
  group("SyncCurrentChapterAction", () {
315
402
  test("is a no-op when book/chapter already match", () async {
316
403
  final bible = buildTestBible();
404
+ final store = Store<AppState>(
317
- final store = Store<AppState>(initialState: AppState(bible: bible, savedBook: 1, savedChapter: 0));
405
+ initialState: AppState(bible: bible, savedBook: 1, savedChapter: 0),
406
+ );
318
407
  final stateBefore = store.state;
319
408
  await store.dispatchAndWait(SyncCurrentChapterAction(1, 0));
320
409
  expect(identical(store.state, stateBefore), isTrue);
@@ -322,19 +411,110 @@ void main() {
322
411
 
323
412
  test("updates state when book/chapter differ", () async {
324
413
  final bible = buildTestBible();
414
+ final store = Store<AppState>(
325
- final store = Store<AppState>(initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0));
415
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
416
+ );
326
417
  await store.dispatchAndWait(SyncCurrentChapterAction(1, 0));
327
418
  expect(store.state.savedBook, 1);
328
419
  expect(store.state.savedChapter, 0);
329
420
  });
330
421
  });
331
422
 
423
+ // These used to be untestable here: they all call stopAudioPlayback(),
424
+ // which touched flutter_soloud's FFI bindings and dlsym'd a native symbol
425
+ // only present once the plugin's native library was loaded by a real app
426
+ // process, crashing in this headless VM regardless of the logic under
427
+ // test. Switching to audioplayers (a plain platform-channel plugin, lazily
428
+ // constructed) removed that: stopAudioPlayback() is a no-op whenever
429
+ // playback was never started, which is always true here.
430
+ group("GoToChapterAction", () {
431
+ test("updates savedBook/savedChapter and pushes the route", () async {
432
+ final bible = buildTestBible();
433
+ final store = Store<AppState>(
434
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
435
+ );
436
+ final router = buildTestRouter();
437
+ await store.dispatchAndWait(GoToChapterAction(router, 2, 1));
438
+ expect(store.state.savedBook, 2);
439
+ expect(store.state.savedChapter, 1);
440
+ });
441
+ });
442
+
332
- group("theme", () {
443
+ group("NextChapterAction", () {
444
+ test("advances to the next chapter and updates saved state", () async {
445
+ final bible = buildTestBible();
446
+ final store = Store<AppState>(
447
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
448
+ );
449
+ final router = buildTestRouter();
450
+ await store.dispatchAndWait(NextChapterAction(router, bible, 0, 0));
451
+ expect(store.state.savedBook, 0);
452
+ expect(store.state.savedChapter, 1);
453
+ });
454
+
333
- test("dark theme labelMedium (verse numbers) uses the dark error accent, matching the light theme's dedicated red", () {
455
+ test("is a no-op after the very last chapter of the last book", () async {
456
+ final bible = buildTestBible();
457
+ final store = Store<AppState>(
334
- expect(AppTheme.dark.textTheme.labelMedium!.color, darkColorScheme.error);
458
+ initialState: AppState(bible: bible, savedBook: 2, savedChapter: 1),
459
+ );
460
+ final router = buildTestRouter();
461
+ await store.dispatchAndWait(NextChapterAction(router, bible, 2, 1));
462
+ expect(store.state.savedBook, 2);
463
+ expect(store.state.savedChapter, 1);
464
+ });
465
+ });
466
+
467
+ group("PreviousChapterAction", () {
468
+ test("moves to the previous chapter and updates saved state", () async {
469
+ final bible = buildTestBible();
470
+ final store = Store<AppState>(
471
+ initialState: AppState(bible: bible, savedBook: 1, savedChapter: 0),
472
+ );
473
+ final router = buildTestRouter();
474
+ await store.dispatchAndWait(PreviousChapterAction(router, bible, 1, 0));
475
+ expect(store.state.savedBook, 0);
476
+ expect(store.state.savedChapter, 1);
335
477
  });
336
478
  });
337
479
 
480
+ group("PopChapterAction", () {
481
+ testWidgets("restores the pre-push savedBook/savedChapter synchronously, "
482
+ "without waiting on the router's pop-transition listener", (
483
+ tester,
484
+ ) async {
485
+ final bible = buildTestBible();
486
+ final store = Store<AppState>(
487
+ initialState: AppState(bible: bible, savedBook: 0, savedChapter: 0),
488
+ );
489
+ final router = buildTestRouter();
490
+ // A mounted Navigator is required for router.pop() to find something
491
+ // to pop — router.push() alone (used by the other actions above)
492
+ // doesn't need one.
493
+ await tester.pumpWidget(MaterialApp.router(routerConfig: router));
494
+
495
+ await store.dispatchAndWait(GoToChapterAction(router, 2, 1));
496
+ await tester.pumpAndSettle();
497
+ expect(store.state.savedBook, 2);
498
+ expect(store.state.savedChapter, 1);
499
+
500
+ await store.dispatchAndWait(PopChapterAction(router));
501
+ expect(store.state.savedBook, 0);
502
+ expect(store.state.savedChapter, 0);
503
+ });
504
+ });
505
+
506
+ group("theme", () {
507
+ test(
508
+ "dark theme labelMedium (verse numbers) uses the dark error accent, matching the light theme's dedicated red",
509
+ () {
510
+ expect(
511
+ AppTheme.dark.textTheme.labelMedium!.color,
512
+ darkColorScheme.error,
513
+ );
514
+ },
515
+ );
516
+ });
517
+
338
518
  group("Env", () {
339
519
  test("obfuscated secrets decode back to non-empty strings", () {
340
520
  expect(Env.errorReportSecret, isNotEmpty);
@@ -354,24 +534,43 @@ void main() {
354
534
  await tempDir.delete(recursive: true);
355
535
  });
356
536
 
537
+ test(
357
- test("persistDifference writes atomically, leaving no leftover temp file", () async {
538
+ "persistDifference writes atomically, leaving no leftover temp file",
539
+ () async {
358
- final bible = buildTestBible();
540
+ final bible = buildTestBible();
359
- final persistor = AppPersistor();
541
+ final persistor = AppPersistor();
542
+ final state = AppState(
543
+ bible: bible,
544
+ savedBook: 1,
545
+ savedChapter: 0,
360
- final state = AppState(bible: bible, savedBook: 1, savedChapter: 0, themeMode: ThemeMode.dark);
546
+ themeMode: ThemeMode.dark,
361
-
547
+ );
548
+
362
- await persistor.persistDifference(lastPersistedState: null, newState: state);
549
+ await persistor.persistDifference(
363
-
550
+ lastPersistedState: null,
551
+ newState: state,
552
+ );
553
+
364
- final json = await persistor.readJson();
554
+ final json = await persistor.readJson();
365
- expect(json, isNotNull);
555
+ expect(json, isNotNull);
366
- expect(json!["savedBook"], 1);
556
+ expect(json!["savedBook"], 1);
367
- expect(json["themeMode"], "dark");
557
+ expect(json["themeMode"], "dark");
558
+ expect(
368
- expect(await File("${tempDir.path}/app_state.json.tmp").exists(), isFalse);
559
+ await File("${tempDir.path}/app_state.json.tmp").exists(),
560
+ isFalse,
369
- });
561
+ );
370
-
562
+ },
563
+ );
564
+
565
+ test(
371
- test("readJson returns null for corrupt JSON instead of throwing", () async {
566
+ "readJson returns null for corrupt JSON instead of throwing",
567
+ () async {
372
- final persistor = AppPersistor();
568
+ final persistor = AppPersistor();
569
+ await File(
373
- await File("${tempDir.path}/app_state.json").writeAsString("{not valid json");
570
+ "${tempDir.path}/app_state.json",
571
+ ).writeAsString("{not valid json");
374
- expect(await persistor.readJson(), isNull);
572
+ expect(await persistor.readJson(), isNull);
573
+ },
375
- });
574
+ );
376
575
  });
377
576
  }
test/screenshot_test.dart CHANGED
@@ -10,7 +10,7 @@ import "package:only_bible_app/gen/bible.gen.dart";
10
10
  import "package:only_bible_app/home.dart";
11
11
  import "package:only_bible_app/store/app_navigator.dart";
12
12
  import "package:only_bible_app/store/app_state.dart";
13
- import "package:shared/shared.dart";
13
+ import "package:only_bible_app/theme.dart";
14
14
  import "package:only_bible_app/utils.dart";
15
15
  import "package:path/path.dart" as path;
16
16