diff --git a/.metadata b/.metadata index df13aa7..53ab8f2 100644 --- a/.metadata +++ b/.metadata @@ -18,6 +18,12 @@ migration: - platform: android create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: ios + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: macos + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 # User provided section diff --git a/android/app/src/main/kotlin/com/jamshalat/jamshalat_diary/MainActivity.kt b/android/app/src/main/kotlin/com/jamshalat/jamshalat_diary/MainActivity.kt new file mode 100644 index 0000000..fa6f75d --- /dev/null +++ b/android/app/src/main/kotlin/com/jamshalat/jamshalat_diary/MainActivity.kt @@ -0,0 +1,5 @@ +package com.jamshalat.jamshalat_diary + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..0bd0fdb 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..70317b5 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..be0e3c1 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..3eee02b 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..bb06f2f 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/local.properties b/android/local.properties index 715bc9e..f89c081 100644 --- a/android/local.properties +++ b/android/local.properties @@ -1,5 +1,5 @@ sdk.dir=/Users/dwindown/Library/Android/sdk -flutter.sdk=/Users/dwindown/FlutterDev/flutter +flutter.sdk=/opt/homebrew/share/flutter flutter.buildMode=release flutter.versionName=1.0.0 flutter.versionCode=1 \ No newline at end of file diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..5c56c23 Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/images/logo_normal.png b/assets/images/logo_normal.png new file mode 100644 index 0000000..bf0b495 Binary files /dev/null and b/assets/images/logo_normal.png differ diff --git a/assets/images/logo_white.png b/assets/images/logo_white.png new file mode 100644 index 0000000..dc360b7 Binary files /dev/null and b/assets/images/logo_white.png differ diff --git a/dzikir-display-mode-ux-brief.md b/dzikir-display-mode-ux-brief.md new file mode 100644 index 0000000..4ad5f55 --- /dev/null +++ b/dzikir-display-mode-ux-brief.md @@ -0,0 +1,81 @@ +# Dzikir Display Mode UX Brief + +## 1) Objective +Provide two complementary experiences for Dzikir: + +- **Daftar (Baris)** for fast scanning and jumping between items. +- **Fokus (Slide)** for one-item focus with consistent thumb reach and counting flow. + +This mode applies to all Dzikir tabs: **Pagi**, **Petang**, and **Sesudah Shalat**. + +## 2) Settings Specification + +Section name in Settings: **Tampilan Dzikir** + +| Label | Type | Options | Default | Visibility | +|---|---|---|---|---| +| `Mode Tampilan Dzikir` | Segmented | `Daftar (Baris)` / `Fokus (Slide)` | `Daftar (Baris)` | Always | +| `Posisi Tombol Hitung` | Segmented | `Pill Bawah (Disarankan)` / `Bulat Kanan Bawah` | `Pill Bawah (Disarankan)` | Only in `Fokus (Slide)` | +| `Lanjut Otomatis Saat Target Tercapai` | Switch | `On/Off` | `On` | Only in `Fokus (Slide)` | +| `Getaran Saat Hitung` | Switch | `On/Off` | `On` | Always | + +## 3) Interaction Rules + +### A. Mode: Daftar (Baris) +- Keep current row-based list and per-row counter pattern. +- Users can scan, jump, and increment any row directly. +- Counter behavior remains per item, per day. + +### B. Mode: Fokus (Slide) +- Display exactly **one dzikir item per slide**. +- Horizontal swipe moves between dzikir items. +- Counter button is fixed in one location (based on selected button position). +- Top area displays progress: `Item X dari Y`. +- Tapping counter increments by `+1` until target. +- When target reached: + - Mark item as complete. + - If `Lanjut Otomatis... = On`, move to next slide automatically (except last item). + +## 4) Button Placement Recommendation + +Primary recommendation: + +- **Pill Bawah (Disarankan)** as default in Focus mode. + +Reason: +- Better one-handed ergonomics. +- Consistent location improves counting rhythm. +- Larger tap target lowers miss taps while reciting. + +Optional style: +- **Bulat Kanan Bawah** for users preferring minimal visual footprint. + +## 5) Data & State Behavior + +- Counter data is shared across modes (switching mode must not reset progress). +- Existing daily tracking logic remains unchanged. +- Switching mode keeps current tab (`Pagi/Petang/Sesudah Shalat`) intact. +- Completed state must be reflected identically in both modes. + +## 6) Completion & Feedback UX + +- Counter states: `normal` and `completed`. +- Completed label example: `Selesai`. +- Last item completion feedback: + - Show subtle confirmation message: `Semua dzikir pada tab ini selesai`. +- Empty or missing data: + - Show friendly empty state, never blank screen. + +## 7) Default Product Decision + +- App default: **Daftar (Baris)** for broad familiarity. +- Advanced/focus users can enable **Fokus (Slide)**. +- In Focus mode, default button placement: **Pill Bawah (Disarankan)**. + +## 8) Success Criteria + +- Users can switch between modes without losing count progress. +- Focus mode reduces hand travel for repeated taps. +- Both modes remain consistent across all Dzikir tabs. +- No behavioral mismatch between count target, completion state, and progress indicator. + diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Generated.xcconfig b/ios/Flutter/Generated.xcconfig index 94693c2..8244bb5 100644 --- a/ios/Flutter/Generated.xcconfig +++ b/ios/Flutter/Generated.xcconfig @@ -1,6 +1,6 @@ // This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/dwindown/FlutterDev/flutter -FLUTTER_APPLICATION_PATH=/Users/dwindown/CascadeProjects/jamshalat-diary +FLUTTER_ROOT=/opt/homebrew/share/flutter +FLUTTER_APPLICATION_PATH=/Users/dwindown/Applications/jamshalat-diary COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_TARGET=lib/main.dart FLUTTER_BUILD_DIR=build diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 0f03ae0..b78d495 100755 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/dwindown/FlutterDev/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/dwindown/CascadeProjects/jamshalat-diary" +export "FLUTTER_ROOT=/opt/homebrew/share/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/dwindown/Applications/jamshalat-diary" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6d7d93b --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,620 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamshalat.jamshalatDiary; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..2bac783 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Jamshalat Diary + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + jamshalat_diary + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/app/router.dart b/lib/app/router.dart index ec112a0..0083036 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -11,11 +11,14 @@ import '../features/checklist/presentation/checklist_screen.dart'; import '../features/laporan/presentation/laporan_screen.dart'; import '../features/tools/presentation/tools_screen.dart'; import '../features/dzikir/presentation/dzikir_screen.dart'; +import '../features/doa/presentation/doa_screen.dart'; +import '../features/hadits/presentation/hadits_screen.dart'; import '../features/qibla/presentation/qibla_screen.dart'; import '../features/quran/presentation/quran_screen.dart'; import '../features/quran/presentation/quran_reading_screen.dart'; import '../features/quran/presentation/quran_murattal_screen.dart'; import '../features/quran/presentation/quran_bookmarks_screen.dart'; +import '../features/quran/presentation/quran_enrichment_screen.dart'; import '../features/settings/presentation/settings_screen.dart'; /// Navigation key for the shell navigator (bottom-nav screens). @@ -79,6 +82,11 @@ final GoRouter appRouter = GoRouter( parentNavigatorKey: _rootNavigatorKey, builder: (context, state) => const QuranScreen(), routes: [ + GoRoute( + path: 'enrichment', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const QuranEnrichmentScreen(), + ), GoRoute( path: 'bookmarks', parentNavigatorKey: _rootNavigatorKey, @@ -116,6 +124,16 @@ final GoRouter appRouter = GoRouter( parentNavigatorKey: _rootNavigatorKey, builder: (context, state) => const QiblaScreen(), ), + GoRoute( + path: 'doa', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const DoaScreen(), + ), + GoRoute( + path: 'hadits', + parentNavigatorKey: _rootNavigatorKey, + builder: (context, state) => const HaditsScreen(), + ), ], ), // Simple Mode Tab: Zikir @@ -128,6 +146,10 @@ final GoRouter appRouter = GoRouter( path: '/quran', builder: (context, state) => const QuranScreen(isSimpleModeTab: true), routes: [ + GoRoute( + path: 'enrichment', + builder: (context, state) => const QuranEnrichmentScreen(), + ), GoRoute( path: 'bookmarks', builder: (context, state) => const QuranBookmarksScreen(), @@ -159,6 +181,14 @@ final GoRouter appRouter = GoRouter( ), ], ), + GoRoute( + path: '/doa', + builder: (context, state) => const DoaScreen(isSimpleModeTab: true), + ), + GoRoute( + path: '/hadits', + builder: (context, state) => const HaditsScreen(isSimpleModeTab: true), + ), ], ), // ── Settings (pushed, no bottom nav) ── diff --git a/lib/app/theme/app_text_styles.dart b/lib/app/theme/app_text_styles.dart index dd66573..bcdd857 100644 --- a/lib/app/theme/app_text_styles.dart +++ b/lib/app/theme/app_text_styles.dart @@ -64,7 +64,7 @@ class AppTextStyles { static const TextStyle arabicLarge = TextStyle( fontFamily: 'Amiri', fontSize: 28, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w400, height: 2.2, ); } diff --git a/lib/data/local/models/app_settings.dart b/lib/data/local/models/app_settings.dart index 9d42cf0..3253130 100644 --- a/lib/data/local/models/app_settings.dart +++ b/lib/data/local/models/app_settings.dart @@ -65,6 +65,18 @@ class AppSettings extends HiveObject { @HiveField(19) bool simpleMode; // false = Mode Lengkap, true = Mode Simpel + @HiveField(20) + String dzikirDisplayMode; // 'list' | 'focus' + + @HiveField(21) + String dzikirCounterButtonPosition; // 'bottomPill' | 'fabCircle' + + @HiveField(22) + bool dzikirAutoAdvance; + + @HiveField(23) + bool dzikirHapticOnCount; + AppSettings({ this.userName = 'User', this.userEmail = '', @@ -86,6 +98,10 @@ class AppSettings extends HiveObject { this.showLatin = true, this.showTerjemahan = true, this.simpleMode = false, + this.dzikirDisplayMode = 'list', + this.dzikirCounterButtonPosition = 'bottomPill', + this.dzikirAutoAdvance = true, + this.dzikirHapticOnCount = true, }) : adhanEnabled = adhanEnabled ?? { 'fajr': true, diff --git a/lib/data/local/models/app_settings.g.dart b/lib/data/local/models/app_settings.g.dart index c6a0cdf..b7a40a5 100644 --- a/lib/data/local/models/app_settings.g.dart +++ b/lib/data/local/models/app_settings.g.dart @@ -37,13 +37,17 @@ class AppSettingsAdapter extends TypeAdapter { showLatin: fields.containsKey(17) ? fields[17] as bool? ?? true : true, showTerjemahan: fields.containsKey(18) ? fields[18] as bool? ?? true : true, simpleMode: fields.containsKey(19) ? fields[19] as bool? ?? false : false, + dzikirDisplayMode: fields.containsKey(20) ? fields[20] as String? ?? 'list' : 'list', + dzikirCounterButtonPosition: fields.containsKey(21) ? fields[21] as String? ?? 'bottomPill' : 'bottomPill', + dzikirAutoAdvance: fields.containsKey(22) ? fields[22] as bool? ?? true : true, + dzikirHapticOnCount: fields.containsKey(23) ? fields[23] as bool? ?? true : true, ); } @override void write(BinaryWriter writer, AppSettings obj) { writer - ..writeByte(20) + ..writeByte(24) ..writeByte(0) ..write(obj.userName) ..writeByte(1) @@ -83,7 +87,15 @@ class AppSettingsAdapter extends TypeAdapter { ..writeByte(18) ..write(obj.showTerjemahan) ..writeByte(19) - ..write(obj.simpleMode); + ..write(obj.simpleMode) + ..writeByte(20) + ..write(obj.dzikirDisplayMode) + ..writeByte(21) + ..write(obj.dzikirCounterButtonPosition) + ..writeByte(22) + ..write(obj.dzikirAutoAdvance) + ..writeByte(23) + ..write(obj.dzikirHapticOnCount); } @override diff --git a/lib/data/services/muslim_api_service.dart b/lib/data/services/muslim_api_service.dart new file mode 100644 index 0000000..36360b3 --- /dev/null +++ b/lib/data/services/muslim_api_service.dart @@ -0,0 +1,561 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class MuslimApiException implements Exception { + final String message; + const MuslimApiException(this.message); + + @override + String toString() => 'MuslimApiException: $message'; +} + +/// Service for muslim.backoffice.biz.id API. +/// +/// Exposes Quran, dzikir, doa, hadits, and enrichment data while preserving +/// the data contract currently expected by Quran and dashboard UI widgets. +class MuslimApiService { + static const String _baseUrl = 'https://muslim.backoffice.biz.id'; + static final MuslimApiService instance = MuslimApiService._(); + + MuslimApiService._(); + + static const Map qariNames = { + '01': 'Abdullah Al-Juhany', + '02': 'Abdul Muhsin Al-Qasim', + '03': 'Abdurrahman As-Sudais', + '04': 'Ibrahim Al-Dossari', + '05': 'Misyari Rasyid Al-Afasi', + '06': 'Yasser Al-Dosari', + }; + + List>? _surahListCache; + final Map> _surahCache = {}; + + List>? _allAyahCache; + List>? _tafsirCache; + List>? _asbabCache; + List>? _juzCache; + List>? _themeCache; + List>? _asmaCache; + List>? _doaCache; + List>? _haditsCache; + + final Map>> _dzikirByTypeCache = {}; + final Map>> _wordByWordCache = {}; + final Map>> _pageAyahCache = {}; + + Future _getData(String path) async { + try { + final response = await http.get(Uri.parse('$_baseUrl$path')); + if (response.statusCode != 200) { + return null; + } + final decoded = json.decode(response.body); + if (decoded is Map) { + return decoded['data']; + } + return null; + } catch (_) { + return null; + } + } + + Future _getDataOrThrow(String path) async { + final response = await http.get(Uri.parse('$_baseUrl$path')); + if (response.statusCode != 200) { + throw MuslimApiException( + 'Request failed ($path): HTTP ${response.statusCode}', + ); + } + + final decoded = json.decode(response.body); + if (decoded is! Map) { + throw const MuslimApiException('Invalid API payload shape'); + } + + final status = _asInt(decoded['status']); + if (status != 200) { + throw MuslimApiException('API returned non-200 status: $status'); + } + + if (!decoded.containsKey('data')) { + throw const MuslimApiException('API payload missing data key'); + } + + return decoded['data']; + } + + int _asInt(dynamic value, {int fallback = 0}) { + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) return int.tryParse(value) ?? fallback; + return fallback; + } + + String _asString(dynamic value, {String fallback = ''}) { + if (value == null) return fallback; + return value.toString(); + } + + int _asCount(dynamic value, {int fallback = 1}) { + if (value == null) return fallback; + if (value is int) return value; + if (value is num) return value.toInt(); + final text = value.toString(); + final match = RegExp(r'\d+').firstMatch(text); + if (match == null) return fallback; + return int.tryParse(match.group(0)!) ?? fallback; + } + + String _stableDzikirId(String type, Map item) { + final apiId = _asString(item['id']); + if (apiId.isNotEmpty) { + return '${type}_$apiId'; + } + + final seed = [ + type, + _asString(item['type']), + _asString(item['arab']), + _asString(item['indo']), + _asString(item['ulang']), + ].join('|'); + + var hash = 0; + for (final unit in seed.codeUnits) { + hash = ((hash * 31) + unit) & 0x7fffffff; + } + return '${type}_$hash'; + } + + String _dzikirApiType(String type) { + switch (type) { + case 'petang': + return 'sore'; + default: + return type; + } + } + + Map _normalizeAudioMap(dynamic audioValue) { + final audioUrl = _asString(audioValue); + if (audioUrl.isEmpty) return {}; + return { + '01': audioUrl, + '02': audioUrl, + '03': audioUrl, + '04': audioUrl, + '05': audioUrl, + '06': audioUrl, + }; + } + + Map _mapSurahSummary(Map item) { + final number = _asInt(item['number']); + return { + 'nomor': number, + 'nama': _asString(item['name_short']), + 'namaLatin': _asString(item['name_id']), + 'jumlahAyat': _asInt(item['number_of_verses']), + 'tempatTurun': _asString(item['revelation_id']), + 'arti': _asString(item['translation_id']), + 'deskripsi': _asString(item['tafsir']), + 'audioFull': _normalizeAudioMap(item['audio_url']), + }; + } + + Map _mapAyah(Map item) { + final audio = _asString(item['audio']); + return { + 'nomorAyat': _asInt(item['ayah']), + 'teksArab': _asString(item['arab']), + 'teksLatin': _asString(item['latin']), + 'teksIndonesia': _asString(item['text']), + 'audio': { + '01': audio, + '02': audio, + '03': audio, + '04': audio, + '05': audio, + '06': audio, + }, + 'juz': _asInt(item['juz']), + 'page': _asInt(item['page']), + 'hizb': _asInt(item['hizb']), + 'theme': _asString(item['theme']), + 'asbab': _asString(item['asbab']), + 'notes': _asString(item['notes']), + 'surah': _asInt(item['surah']), + 'ayahId': _asInt(item['id']), + }; + } + + Future>> getAllSurahs() async { + if (_surahListCache != null) return _surahListCache!; + final raw = await _getData('/v1/quran/surah'); + if (raw is! List) return []; + _surahListCache = raw + .whereType>() + .map(_mapSurahSummary) + .toList(); + return _surahListCache!; + } + + Future?> getSurah(int number) async { + if (_surahCache.containsKey(number)) { + return _surahCache[number]; + } + + final surahs = await getAllSurahs(); + Map? summary; + for (final surah in surahs) { + if (surah['nomor'] == number) { + summary = surah; + break; + } + } + final rawAyah = await _getData('/v1/quran/ayah/surah?id=$number'); + if (summary == null || rawAyah is! List) { + return null; + } + + final mappedAyah = rawAyah + .whereType>() + .map(_mapAyah) + .toList(); + + final mapped = { + ...summary, + 'ayat': mappedAyah, + }; + _surahCache[number] = mapped; + return mapped; + } + + Future?> getDailyAyat() async { + try { + final now = DateTime.now(); + final dayOfYear = now.difference(DateTime(now.year, 1, 1)).inDays; + final surahId = (dayOfYear % 114) + 1; + final surah = await getSurah(surahId); + if (surah == null) return null; + + final ayat = List>.from(surah['ayat'] ?? []); + if (ayat.isEmpty) return null; + + final ayatIndex = dayOfYear % ayat.length; + final picked = ayat[ayatIndex]; + return { + 'surahName': surah['namaLatin'] ?? '', + 'nomorSurah': surahId, + 'nomorAyat': picked['nomorAyat'] ?? 1, + 'teksArab': picked['teksArab'] ?? '', + 'teksIndonesia': picked['teksIndonesia'] ?? '', + }; + } catch (_) { + return null; + } + } + + Future>> getWordByWord(int surahId, int ayahId) async { + final key = '$surahId:$ayahId'; + if (_wordByWordCache.containsKey(key)) return _wordByWordCache[key]!; + + final raw = await _getData('/v1/quran/word/ayah?surahId=$surahId&ayahId=$ayahId'); + if (raw is! List) return []; + + final mapped = raw.whereType>().map((item) { + return { + 'word': _asString(item['word']), + 'arab': _asString(item['arab']), + 'indo': _asString(item['indo']), + }; + }).toList(); + + _wordByWordCache[key] = mapped; + return mapped; + } + + Future>> getAllAyah() async { + if (_allAyahCache != null) return _allAyahCache!; + final raw = await _getData('/v1/quran/ayah'); + if (raw is! List) return []; + + _allAyahCache = raw.whereType>().map((item) { + return { + 'id': _asInt(item['id']), + 'surah': _asInt(item['surah']), + 'ayah': _asInt(item['ayah']), + 'arab': _asString(item['arab']), + 'latin': _asString(item['latin']), + 'text': _asString(item['text']), + 'juz': _asInt(item['juz']), + 'page': _asInt(item['page']), + 'hizb': _asInt(item['hizb']), + 'theme': _asString(item['theme']), + 'asbab': _asString(item['asbab']), + }; + }).toList(); + + return _allAyahCache!; + } + + Future>> getTafsirBySurah(int surahId) async { + if (_tafsirCache == null) { + final raw = await _getData('/v1/quran/tafsir'); + if (raw is! List) return []; + _tafsirCache = raw.whereType>().map((item) { + return { + 'id': _asInt(item['id']), + 'ayah': _asInt(item['ayah']), + 'wajiz': _asString(item['wajiz']), + 'tahlili': _asString(item['tahlili']), + }; + }).toList(); + } + + final allAyah = await getAllAyah(); + if (allAyah.isEmpty || _tafsirCache == null) return []; + + final ayahById = >{}; + final ayahBySurahAyah = >{}; + for (final ayah in allAyah) { + final id = _asInt(ayah['id']); + final surah = _asInt(ayah['surah']); + final ayahNumber = _asInt(ayah['ayah']); + ayahById[id] = ayah; + ayahBySurahAyah['$surah:$ayahNumber'] = ayah; + } + + final result = >[]; + for (final tafsir in _tafsirCache!) { + final tafsirId = _asInt(tafsir['id']); + final tafsirAyah = _asInt(tafsir['ayah']); + Map? ayahMeta = ayahById[tafsirId]; + ayahMeta ??= ayahBySurahAyah['$surahId:$tafsirAyah']; + if (ayahMeta == null) continue; + if (ayahMeta['surah'] != surahId) continue; + result.add({ + 'nomorAyat': _asInt(ayahMeta['ayah'], fallback: tafsirAyah), + 'wajiz': tafsir['wajiz'], + 'tahlili': tafsir['tahlili'], + }); + } + + result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int)); + return result; + } + + Future>> getAsbabBySurah(int surahId) async { + if (_asbabCache == null) { + final raw = await _getData('/v1/quran/asbab'); + if (raw is! List) return []; + _asbabCache = raw.whereType>().map((item) { + return { + 'id': _asInt(item['id']), + 'ayah': _asInt(item['ayah']), + 'text': _asString(item['text']), + }; + }).toList(); + } + + final allAyah = await getAllAyah(); + if (allAyah.isEmpty || _asbabCache == null) return []; + + final ayahById = >{}; + final ayahBySurahAyah = >{}; + for (final ayah in allAyah) { + final id = _asInt(ayah['id']); + final surah = _asInt(ayah['surah']); + final ayahNumber = _asInt(ayah['ayah']); + ayahById[id] = ayah; + ayahBySurahAyah['$surah:$ayahNumber'] = ayah; + } + + final result = >[]; + for (final asbab in _asbabCache!) { + final asbabId = _asInt(asbab['id']); + final asbabAyah = _asInt(asbab['ayah']); + Map? ayahMeta = ayahById[asbabId]; + ayahMeta ??= ayahBySurahAyah['$surahId:$asbabAyah']; + if (ayahMeta == null) continue; + if (ayahMeta['surah'] != surahId) continue; + result.add({ + 'nomorAyat': _asInt(ayahMeta['ayah'], fallback: asbabAyah), + 'text': asbab['text'], + }); + } + + result.sort((a, b) => (a['nomorAyat'] as int).compareTo(b['nomorAyat'] as int)); + return result; + } + + Future>> getJuzList() async { + if (_juzCache != null) return _juzCache!; + final raw = await _getData('/v1/quran/juz'); + if (raw is! List) return []; + + _juzCache = raw.whereType>().map((item) { + return { + 'number': _asInt(item['number']), + 'name': _asString(item['name']), + 'surah_id_start': _asInt(item['surah_id_start']), + 'verse_start': _asInt(item['verse_start']), + 'surah_id_end': _asInt(item['surah_id_end']), + 'verse_end': _asInt(item['verse_end']), + 'name_start_id': _asString(item['name_start_id']), + 'name_end_id': _asString(item['name_end_id']), + }; + }).toList(); + + return _juzCache!; + } + + Future>> getAyahByPage(int page) async { + if (_pageAyahCache.containsKey(page)) return _pageAyahCache[page]!; + final raw = await _getData('/v1/quran/ayah/page?id=$page'); + if (raw is! List) return []; + + final mapped = raw.whereType>().map((item) { + return { + 'surah': _asInt(item['surah']), + 'ayah': _asInt(item['ayah']), + 'arab': _asString(item['arab']), + 'text': _asString(item['text']), + 'theme': _asString(item['theme']), + }; + }).toList(); + + _pageAyahCache[page] = mapped; + return mapped; + } + + Future>> getThemes() async { + if (_themeCache != null) return _themeCache!; + final raw = await _getData('/v1/quran/theme'); + if (raw is! List) return []; + + _themeCache = raw.whereType>().map((item) { + return { + 'id': _asInt(item['id']), + 'name': _asString(item['name']), + }; + }).toList(); + return _themeCache!; + } + + Future>> searchAyah(String query) async { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return []; + + final allAyah = await getAllAyah(); + final results = allAyah.where((item) { + final text = _asString(item['text']).toLowerCase(); + final latin = _asString(item['latin']).toLowerCase(); + final arab = _asString(item['arab']); + return text.contains(q) || latin.contains(q) || arab.contains(query.trim()); + }).take(50).toList(); + + return results; + } + + Future>> getAsmaulHusna() async { + if (_asmaCache != null) return _asmaCache!; + final raw = await _getData('/v1/quran/asma'); + if (raw is! List) return []; + + _asmaCache = raw.whereType>().map((item) { + return { + 'id': _asInt(item['id']), + 'arab': _asString(item['arab']), + 'latin': _asString(item['latin']), + 'indo': _asString(item['indo']), + }; + }).toList(); + + return _asmaCache!; + } + + Future>> getDoaList({bool strict = false}) async { + if (_doaCache != null) return _doaCache!; + final raw = strict + ? await _getDataOrThrow('/v1/doa') + : await _getData('/v1/doa'); + if (raw is! List) { + if (strict) { + throw const MuslimApiException('Invalid doa payload'); + } + return []; + } + + _doaCache = raw.whereType>().map((item) { + return { + 'judul': _asString(item['judul']), + 'arab': _asString(item['arab']), + 'indo': _asString(item['indo']), + 'source': _asString(item['source']), + }; + }).toList(); + + return _doaCache!; + } + + Future>> getHaditsList({bool strict = false}) async { + if (_haditsCache != null) return _haditsCache!; + final raw = strict + ? await _getDataOrThrow('/v1/hadits') + : await _getData('/v1/hadits'); + if (raw is! List) { + if (strict) { + throw const MuslimApiException('Invalid hadits payload'); + } + return []; + } + + _haditsCache = raw.whereType>().map((item) { + return { + 'no': _asInt(item['no']), + 'judul': _asString(item['judul']), + 'arab': _asString(item['arab']), + 'indo': _asString(item['indo']), + }; + }).toList(); + + return _haditsCache!; + } + + Future>> getDzikirByType( + String type, { + bool strict = false, + }) async { + if (_dzikirByTypeCache.containsKey(type)) { + return _dzikirByTypeCache[type]!; + } + final apiType = _dzikirApiType(type); + final raw = strict + ? await _getDataOrThrow('/v1/dzikir?type=$apiType') + : await _getData('/v1/dzikir?type=$apiType'); + if (raw is! List) { + if (strict) { + throw MuslimApiException('Invalid dzikir payload for type: $type'); + } + return []; + } + + final mapped = >[]; + for (var i = 0; i < raw.length; i++) { + final item = raw[i]; + if (item is! Map) continue; + mapped.add({ + 'id': _stableDzikirId(type, item), + 'arab': _asString(item['arab']), + 'indo': _asString(item['indo']), + 'type': _asString(item['type']), + 'ulang': _asCount(item['ulang'], fallback: 1), + }); + } + + _dzikirByTypeCache[type] = mapped; + return mapped; + } +} diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart index 120063f..3bc5d90 100644 --- a/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -11,7 +11,7 @@ import '../../../core/widgets/tool_card.dart'; import '../../../data/local/hive_boxes.dart'; import '../../../data/local/models/app_settings.dart'; import '../../../data/local/models/daily_worship_log.dart'; -import '../../../data/services/equran_service.dart'; +import '../../../data/services/muslim_api_service.dart'; import '../data/prayer_times_provider.dart'; class DashboardScreen extends ConsumerStatefulWidget { @@ -810,13 +810,57 @@ class _DashboardScreenState extends ConsumerState { ), ], ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ToolCard( + icon: LucideIcons.heart, + title: 'Kumpulan\nDoa', + color: const Color(0xFFE17055), + isDark: isDark, + onTap: () { + final isSimple = Hive.box(HiveBoxes.settings) + .get('default') + ?.simpleMode ?? + false; + if (isSimple) { + context.push('/doa'); + } else { + context.push('/tools/doa'); + } + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: ToolCard( + icon: LucideIcons.library, + title: "Hadits\nArba'in", + color: const Color(0xFF6C5CE7), + isDark: isDark, + onTap: () { + final isSimple = Hive.box(HiveBoxes.settings) + .get('default') + ?.simpleMode ?? + false; + if (isSimple) { + context.push('/hadits'); + } else { + context.push('/tools/hadits'); + } + }, + ), + ), + ], + ), ], ); } Widget _buildAyatHariIni(BuildContext context, bool isDark) { return FutureBuilder?>( - future: EQuranService.instance.getDailyAyat(), + future: MuslimApiService.instance.getDailyAyat(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container( @@ -870,6 +914,7 @@ class _DashboardScreenState extends ConsumerState { style: const TextStyle( fontFamily: 'Amiri', fontSize: 24, + fontWeight: FontWeight.w400, height: 1.8, ), textAlign: TextAlign.right, diff --git a/lib/features/doa/presentation/doa_screen.dart b/lib/features/doa/presentation/doa_screen.dart new file mode 100644 index 0000000..b2566c2 --- /dev/null +++ b/lib/features/doa/presentation/doa_screen.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/services/muslim_api_service.dart'; + +class DoaScreen extends StatefulWidget { + final bool isSimpleModeTab; + const DoaScreen({super.key, this.isSimpleModeTab = false}); + + @override + State createState() => _DoaScreenState(); +} + +class _DoaScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + List> _allDoa = []; + List> _filteredDoa = []; + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadDoa(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadDoa() async { + setState(() { + _loading = true; + _error = null; + }); + + try { + final data = await MuslimApiService.instance.getDoaList(strict: true); + if (!mounted) return; + setState(() { + _allDoa = data; + _filteredDoa = data; + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _allDoa = []; + _filteredDoa = []; + _loading = false; + _error = 'Gagal memuat doa dari server'; + }); + } + } + + void _onSearchChanged(String value) { + final q = value.trim().toLowerCase(); + if (q.isEmpty) { + setState(() => _filteredDoa = _allDoa); + return; + } + setState(() { + _filteredDoa = _allDoa.where((item) { + final title = item['judul']?.toString().toLowerCase() ?? ''; + final indo = item['indo']?.toString().toLowerCase() ?? ''; + return title.contains(q) || indo.contains(q); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: !widget.isSimpleModeTab, + title: const Text('Kumpulan Doa'), + actions: [ + IconButton( + onPressed: _loadDoa, + icon: const Icon(LucideIcons.refreshCw), + tooltip: 'Muat ulang', + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: TextField( + controller: _searchController, + onChanged: _onSearchChanged, + decoration: InputDecoration( + hintText: 'Cari judul atau isi doa...', + prefixIcon: const Icon(LucideIcons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Text( + _error!, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : _filteredDoa.isEmpty + ? Center( + child: Text( + 'Doa tidak ditemukan', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + itemCount: _filteredDoa.length, + itemBuilder: (context, index) { + final item = _filteredDoa[index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['judul']?.toString() ?? '-', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: Text( + item['arab']?.toString() ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + fontWeight: FontWeight.w400, + height: 1.8, + ), + ), + ), + const SizedBox(height: 8), + Text( + item['indo']?.toString() ?? '', + style: TextStyle( + height: 1.5, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + if ((item['source']?.toString().isNotEmpty ?? + false)) ...[ + const SizedBox(height: 10), + Text( + 'Sumber: ${item['source']}', + style: const TextStyle( + fontSize: 12, + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/dzikir/presentation/dzikir_screen.dart b/lib/features/dzikir/presentation/dzikir_screen.dart index 8436359..c07fcf5 100644 --- a/lib/features/dzikir/presentation/dzikir_screen.dart +++ b/lib/features/dzikir/presentation/dzikir_screen.dart @@ -1,14 +1,15 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lucide_icons/lucide_icons.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + import '../../../app/theme/app_colors.dart'; import '../../../data/local/hive_boxes.dart'; -import '../../../data/local/models/dzikir_counter.dart'; import '../../../data/local/models/app_settings.dart'; +import '../../../data/local/models/dzikir_counter.dart'; +import '../../../data/services/muslim_api_service.dart'; class DzikirScreen extends ConsumerStatefulWidget { final bool isSimpleModeTab; @@ -21,15 +22,36 @@ class DzikirScreen extends ConsumerStatefulWidget { class _DzikirScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; + + final Map _pageControllers = { + 'pagi': PageController(), + 'petang': PageController(), + 'solat': PageController(), + }; + + final Map _focusPageIndex = { + 'pagi': 0, + 'petang': 0, + 'solat': 0, + }; + List> _pagiItems = []; List> _petangItems = []; + List> _sesudahSholatItems = []; + bool _loading = true; + String? _error; + late Box _counterBox; late String _todayKey; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(() { + if (!mounted) return; + setState(() {}); + }); _counterBox = Hive.box(HiveBoxes.dzikirCounters); _todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now()); _loadData(); @@ -38,17 +60,68 @@ class _DzikirScreenState extends ConsumerState @override void dispose() { _tabController.dispose(); + for (final controller in _pageControllers.values) { + controller.dispose(); + } super.dispose(); } Future _loadData() async { - final pagiJson = - await rootBundle.loadString('assets/dzikir/dzikir_pagi.json'); - final petangJson = - await rootBundle.loadString('assets/dzikir/dzikir_petang.json'); setState(() { - _pagiItems = List>.from(json.decode(pagiJson)); - _petangItems = List>.from(json.decode(petangJson)); + _loading = true; + _error = null; + }); + + try { + final pagi = await MuslimApiService.instance.getDzikirByType( + 'pagi', + strict: true, + ); + final petang = await MuslimApiService.instance.getDzikirByType( + 'petang', + strict: true, + ); + final solat = await MuslimApiService.instance.getDzikirByType( + 'solat', + strict: true, + ); + + if (!mounted) return; + setState(() { + _pagiItems = pagi; + _petangItems = petang; + _sesudahSholatItems = solat; + _loading = false; + }); + _ensureValidFocusPages(); + } catch (_) { + if (!mounted) return; + setState(() { + _loading = false; + _error = 'Gagal memuat dzikir dari server'; + }); + } + } + + void _ensureValidFocusPages() { + _clampFocusPageForPrefix('pagi', _pagiItems.length); + _clampFocusPageForPrefix('petang', _petangItems.length); + _clampFocusPageForPrefix('solat', _sesudahSholatItems.length); + } + + void _clampFocusPageForPrefix(String prefix, int itemLength) { + final maxIndex = itemLength > 0 ? itemLength - 1 : 0; + final current = _focusPageIndex[prefix] ?? 0; + final next = current > maxIndex ? maxIndex : current; + _focusPageIndex[prefix] = next; + + final controller = _pageControllers[prefix]; + if (controller == null || !controller.hasClients) return; + if (controller.page?.round() == next) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || !controller.hasClients) return; + controller.jumpToPage(next); }); } @@ -63,9 +136,15 @@ class _DzikirScreenState extends ConsumerState ); } - void _increment(String dzikirId, int target) { + bool _increment( + String dzikirId, + int target, { + required bool hapticEnabled, + }) { final key = '${dzikirId}_$_todayKey'; var counter = _counterBox.get(key); + final wasComplete = counter != null && counter.count >= counter.target; + if (counter == null) { counter = DzikirCounter( dzikirId: dzikirId, @@ -74,40 +153,42 @@ class _DzikirScreenState extends ConsumerState target: target, ); _counterBox.put(key, counter); - } else { - if (counter.count < counter.target) { - counter.count++; - counter.save(); - } + } else if (counter.count < counter.target) { + counter.count++; + counter.save(); + } + + final isCompleteNow = counter.count >= counter.target; + if (hapticEnabled) { + HapticFeedback.lightImpact(); } setState(() {}); - // Haptic feedback - HapticFeedback.lightImpact(); + return !wasComplete && isCompleteNow; } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - final box = Hive.box(HiveBoxes.settings); - final isSimpleMode = box.get('default')?.simpleMode ?? false; - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: !widget.isSimpleModeTab, - title: const Text('Dzikir Pagi & Petang'), - actions: [ - IconButton( - onPressed: () {}, - icon: const Icon(LucideIcons.info), - ), - ], - ), - body: Column( - children: [ - // Tabs - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: TabBar( + return ValueListenableBuilder>( + valueListenable: + Hive.box(HiveBoxes.settings).listenable(keys: ['default']), + builder: (_, settingsBox, __) { + final settings = settingsBox.get('default') ?? AppSettings(); + final isFocusMode = settings.dzikirDisplayMode == 'focus'; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: !widget.isSimpleModeTab, + title: const Text('Dzikir Harian'), + actions: [ + IconButton( + onPressed: _loadData, + icon: const Icon(LucideIcons.refreshCw), + tooltip: 'Muat ulang', + ), + ], + bottom: TabBar( controller: _tabController, labelColor: AppColors.primary, unselectedLabelColor: isDark @@ -116,47 +197,151 @@ class _DzikirScreenState extends ConsumerState indicatorColor: AppColors.primary, indicatorWeight: 3, labelStyle: - const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + const TextStyle(fontWeight: FontWeight.w700, fontSize: 13), tabs: const [ Tab(text: 'Pagi'), Tab(text: 'Petang'), + Tab(text: 'Sesudah Sholat'), ], ), ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildDzikirList(context, isDark, _pagiItems, 'pagi', - 'Dzikir Pagi', 'Dibaca setelah shalat Shubuh hingga terbit matahari'), - _buildDzikirList(context, isDark, _petangItems, 'petang', - 'Dzikir Petang', 'Dibaca setelah shalat Ashar hingga terbenam matahari'), - ], + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? _buildErrorState(isDark) + : TabBarView( + controller: _tabController, + children: [ + isFocusMode + ? _buildFocusModeTab( + context, + isDark, + settings, + items: _pagiItems, + prefix: 'pagi', + title: 'Dzikir Pagi', + subtitle: + 'Dibaca setelah shalat Subuh hingga terbit matahari.', + ) + : _buildDzikirList( + context, + isDark, + settings, + _pagiItems, + 'pagi', + 'Dzikir Pagi', + 'Dibaca setelah shalat Subuh hingga terbit matahari.', + ), + isFocusMode + ? _buildFocusModeTab( + context, + isDark, + settings, + items: _petangItems, + prefix: 'petang', + title: 'Dzikir Petang', + subtitle: + 'Dibaca setelah Ashar hingga terbenam matahari.', + ) + : _buildDzikirList( + context, + isDark, + settings, + _petangItems, + 'petang', + 'Dzikir Petang', + 'Dibaca setelah Ashar hingga terbenam matahari.', + ), + isFocusMode + ? _buildFocusModeTab( + context, + isDark, + settings, + items: _sesudahSholatItems, + prefix: 'solat', + title: 'Dzikir Sesudah Sholat', + subtitle: + 'Dibaca setelah shalat fardhu sesuai kebutuhan.', + ) + : _buildDzikirList( + context, + isDark, + settings, + _sesudahSholatItems, + 'solat', + 'Dzikir Sesudah Sholat', + 'Dibaca setelah shalat fardhu sesuai kebutuhan.', + ), + ], + ), + ); + }, + ); + } + + Widget _buildErrorState(bool isDark) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.wifiOff, + size: 42, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, ), - ), - ], + const SizedBox(height: 12), + Text( + _error!, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), ), ); } - Widget _buildDzikirList(BuildContext context, bool isDark, - List> items, String prefix, String title, String subtitle) { + Widget _buildDzikirList( + BuildContext context, + bool isDark, + AppSettings settings, + List> items, + String prefix, + String title, + String subtitle, + ) { if (items.isEmpty) { - return const Center(child: CircularProgressIndicator()); + return _buildEmptyState( + isDark, + title: 'Belum ada data dzikir', + subtitle: 'Data untuk tab ini belum tersedia.', + ); } return ListView.builder( padding: const EdgeInsets.all(16), - itemCount: items.length + 1, // +1 for header + itemCount: items.length + 1, itemBuilder: (context, index) { if (index == 0) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Column( children: [ - Text(title, - style: const TextStyle( - fontSize: 22, fontWeight: FontWeight.w800)), + Text( + title, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + ), + ), const SizedBox(height: 4), Text( subtitle, @@ -174,8 +359,8 @@ class _DzikirScreenState extends ConsumerState } final item = items[index - 1]; - final dzikirId = '${prefix}_${item['id']}'; - final target = (item['count'] as num?)?.toInt() ?? 1; + final dzikirId = _resolveDzikirId(item, prefix, index - 1); + final target = (item['ulang'] as num?)?.toInt() ?? 1; final counter = _getCounter(dzikirId, target); final isComplete = counter.count >= counter.target; @@ -197,13 +382,14 @@ class _DzikirScreenState extends ConsumerState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header row: count badge + number Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 4), + horizontal: 10, + vertical: 4, + ), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(50), @@ -230,44 +416,37 @@ class _DzikirScreenState extends ConsumerState ], ), const SizedBox(height: 16), - // Arabic text SizedBox( width: double.infinity, child: Text( - item['arabic'] ?? '', + item['arab']?.toString() ?? '', textAlign: TextAlign.right, style: const TextStyle( fontFamily: 'Amiri', fontSize: 24, + fontWeight: FontWeight.w400, height: 2.0, ), ), ), - const SizedBox(height: 12), - // Transliteration + const SizedBox(height: 10), Text( - item['transliteration'] ?? '', - style: TextStyle( - fontSize: 13, - fontStyle: FontStyle.italic, - color: AppColors.primary, - ), - ), - const SizedBox(height: 8), - // Translation - Text( - '"${item['translation'] ?? ''}"', + '"${item['indo']?.toString() ?? ''}"', style: TextStyle( fontSize: 13, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + height: 1.5, ), ), const SizedBox(height: 16), - // Counter button GestureDetector( - onTap: () => _increment(dzikirId, target), + onTap: () => _increment( + dzikirId, + target, + hapticEnabled: settings.dzikirHapticOnCount, + ), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14), @@ -281,7 +460,9 @@ class _DzikirScreenState extends ConsumerState mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - isComplete ? LucideIcons.check : LucideIcons.fingerprint, + isComplete + ? LucideIcons.check + : LucideIcons.fingerprint, size: 18, color: isComplete ? AppColors.primary @@ -289,7 +470,7 @@ class _DzikirScreenState extends ConsumerState ), const SizedBox(width: 8), Text( - '${counter.count} / $target', + isComplete ? 'Selesai' : '${counter.count} / $target', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, @@ -309,4 +490,433 @@ class _DzikirScreenState extends ConsumerState }, ); } + + Widget _buildFocusModeTab( + BuildContext context, + bool isDark, + AppSettings settings, { + required List> items, + required String prefix, + required String title, + required String subtitle, + }) { + if (items.isEmpty) { + return _buildEmptyState( + isDark, + title: 'Belum ada data dzikir', + subtitle: 'Data untuk tab ini belum tersedia.', + ); + } + + final controller = _pageControllers[prefix]!; + final rawCurrent = _focusPageIndex[prefix] ?? 0; + final currentIndex = rawCurrent.clamp(0, items.length - 1); + final currentItem = items[currentIndex]; + final currentId = _resolveDzikirId(currentItem, prefix, currentIndex); + final currentTarget = (currentItem['ulang'] as num?)?.toInt() ?? 1; + final currentCounter = _getCounter(currentId, currentTarget); + final isComplete = currentCounter.count >= currentCounter.target; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + children: [ + Text( + title, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800), + ), + const SizedBox(height: 4), + Text( + subtitle, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + fontSize: 13, + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + 'Item ${currentIndex + 1} dari ${items.length}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + const SizedBox(height: 12), + Expanded( + child: Stack( + children: [ + PageView.builder( + controller: controller, + itemCount: items.length, + onPageChanged: (index) { + setState(() { + _focusPageIndex[prefix] = index; + }); + }, + itemBuilder: (context, index) { + final item = items[index]; + final dzikirId = _resolveDzikirId(item, prefix, index); + final target = (item['ulang'] as num?)?.toInt() ?? 1; + final counter = _getCounter(dzikirId, target); + final complete = counter.count >= counter.target; + return Padding( + padding: const EdgeInsets.only(bottom: 92), + child: _buildFocusCard( + isDark, + item: item, + index: index, + target: target, + counter: counter, + isComplete: complete, + ), + ); + }, + ), + if (settings.dzikirCounterButtonPosition == 'fabCircle') + Positioned( + right: 8, + bottom: 12, + child: _buildFocusCounterFab( + isDark, + isComplete: isComplete, + label: isComplete + ? 'Selesai' + : '${currentCounter.count}/$currentTarget', + onTap: () => _onFocusCounterTap( + context, + settings, + prefix, + items, + ), + ), + ) + else + Positioned( + left: 0, + right: 0, + bottom: 12, + child: _buildFocusCounterPill( + isComplete: isComplete, + label: isComplete + ? 'Selesai' + : '${currentCounter.count} / $currentTarget', + onTap: () => _onFocusCounterTap( + context, + settings, + prefix, + items, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFocusCard( + bool isDark, { + required Map item, + required int index, + required int target, + required DzikirCounter counter, + required bool isComplete, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isComplete + ? AppColors.primary.withValues(alpha: 0.3) + : (isDark + ? AppColors.primary.withValues(alpha: 0.08) + : AppColors.cream), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + '$target KALI', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + Text( + '${(index + 1).toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: Text( + item['arab']?.toString() ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 28, + fontWeight: FontWeight.w400, + height: 2.0, + ), + ), + ), + const SizedBox(height: 14), + Text( + '"${item['indo']?.toString() ?? ''}"', + style: TextStyle( + fontSize: 14, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + height: 1.6, + ), + ), + const SizedBox(height: 12), + if (isComplete) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(50), + ), + child: Text( + 'Selesai (${counter.count}/$target)', + style: TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildFocusCounterPill({ + required bool isComplete, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isComplete + ? AppColors.primary.withValues(alpha: 0.15) + : AppColors.primary, + borderRadius: BorderRadius.circular(50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isComplete ? LucideIcons.check : LucideIcons.fingerprint, + size: 18, + color: isComplete ? AppColors.primary : AppColors.onPrimary, + ), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: isComplete ? AppColors.primary : AppColors.onPrimary, + ), + ), + ], + ), + ), + ); + } + + Widget _buildFocusCounterFab( + bool isDark, { + required bool isComplete, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isComplete + ? AppColors.primary.withValues(alpha: 0.15) + : AppColors.primary, + boxShadow: [ + BoxShadow( + color: (isDark ? Colors.black : Colors.black26) + .withValues(alpha: 0.14), + blurRadius: 18, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isComplete ? LucideIcons.check : LucideIcons.fingerprint, + size: 18, + color: isComplete ? AppColors.primary : AppColors.onPrimary, + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: isComplete ? AppColors.primary : AppColors.onPrimary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + void _onFocusCounterTap( + BuildContext context, + AppSettings settings, + String prefix, + List> items, + ) { + if (items.isEmpty) return; + + final currentIndex = (_focusPageIndex[prefix] ?? 0).clamp(0, items.length - 1); + final item = items[currentIndex]; + final dzikirId = _resolveDzikirId(item, prefix, currentIndex); + final target = (item['ulang'] as num?)?.toInt() ?? 1; + + final becameComplete = _increment( + dzikirId, + target, + hapticEnabled: settings.dzikirHapticOnCount, + ); + + if (!becameComplete) return; + + final isLast = currentIndex == items.length - 1; + if (settings.dzikirAutoAdvance && !isLast) { + final controller = _pageControllers[prefix]; + if (controller != null && controller.hasClients) { + controller.nextPage( + duration: const Duration(milliseconds: 240), + curve: Curves.easeOut, + ); + } + return; + } + + if (isLast) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Semua dzikir pada tab ini selesai'), + duration: Duration(seconds: 2), + ), + ); + } + } + + String _resolveDzikirId(Map item, String prefix, int index) { + final rawId = item['id']?.toString(); + if (rawId != null && rawId.isNotEmpty) { + return rawId; + } + return '${prefix}_${index + 1}'; + } + + Widget _buildEmptyState( + bool isDark, { + required String title, + required String subtitle, + }) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.inbox, + size: 42, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + const SizedBox(height: 12), + Text( + title, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + subtitle, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } } diff --git a/lib/features/hadits/presentation/hadits_screen.dart b/lib/features/hadits/presentation/hadits_screen.dart new file mode 100644 index 0000000..307afc6 --- /dev/null +++ b/lib/features/hadits/presentation/hadits_screen.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/services/muslim_api_service.dart'; + +class HaditsScreen extends StatefulWidget { + final bool isSimpleModeTab; + const HaditsScreen({super.key, this.isSimpleModeTab = false}); + + @override + State createState() => _HaditsScreenState(); +} + +class _HaditsScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + List> _allHadits = []; + List> _filteredHadits = []; + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadHadits(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadHadits() async { + setState(() { + _loading = true; + _error = null; + }); + + try { + final data = await MuslimApiService.instance.getHaditsList(strict: true); + if (!mounted) return; + data.sort((a, b) { + final aa = (a['no'] as num?)?.toInt() ?? 0; + final bb = (b['no'] as num?)?.toInt() ?? 0; + return aa.compareTo(bb); + }); + setState(() { + _allHadits = data; + _filteredHadits = data; + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _allHadits = []; + _filteredHadits = []; + _loading = false; + _error = 'Gagal memuat hadits dari server'; + }); + } + } + + void _onSearchChanged(String value) { + final q = value.trim().toLowerCase(); + if (q.isEmpty) { + setState(() => _filteredHadits = _allHadits); + return; + } + setState(() { + _filteredHadits = _allHadits.where((item) { + final title = item['judul']?.toString().toLowerCase() ?? ''; + final indo = item['indo']?.toString().toLowerCase() ?? ''; + return title.contains(q) || indo.contains(q); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: !widget.isSimpleModeTab, + title: const Text("Hadits Arba'in"), + actions: [ + IconButton( + onPressed: _loadHadits, + icon: const Icon(LucideIcons.refreshCw), + tooltip: 'Muat ulang', + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: TextField( + controller: _searchController, + onChanged: _onSearchChanged, + decoration: InputDecoration( + hintText: 'Cari judul atau isi hadits...', + prefixIcon: const Icon(LucideIcons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Text( + _error!, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : _filteredHadits.isEmpty + ? Center( + child: Text( + 'Hadits tidak ditemukan', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + itemCount: _filteredHadits.length, + itemBuilder: (context, index) { + final item = _filteredHadits[index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 34, + height: 34, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.primary + .withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${item['no'] ?? '-'}', + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item['judul']?.toString() ?? '-', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: Text( + item['arab']?.toString() ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + fontWeight: FontWeight.w400, + height: 1.8, + ), + ), + ), + const SizedBox(height: 8), + Text( + item['indo']?.toString() ?? '', + style: TextStyle( + height: 1.5, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/quran/presentation/quran_bookmarks_screen.dart b/lib/features/quran/presentation/quran_bookmarks_screen.dart index d13812e..e805bb3 100644 --- a/lib/features/quran/presentation/quran_bookmarks_screen.dart +++ b/lib/features/quran/presentation/quran_bookmarks_screen.dart @@ -19,6 +19,14 @@ class _QuranBookmarksScreenState extends State { bool _showLatin = true; bool _showTerjemahan = true; + String _readingRoute(int surahId, int verseId) { + final isSimple = + Hive.box(HiveBoxes.settings).get('default')?.simpleMode ?? + false; + final base = isSimple ? '/quran' : '/tools/quran'; + return '$base/$surahId?startVerse=$verseId'; + } + @override void initState() { super.initState(); @@ -184,7 +192,7 @@ class _QuranBookmarksScreenState extends State { final dateStr = DateFormat('dd MMM yyyy, HH:mm').format(bookmark.savedAt); return InkWell( - onTap: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'), + onTap: () => context.push(_readingRoute(bookmark.surahId, bookmark.verseId)), borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(16), @@ -252,6 +260,7 @@ class _QuranBookmarksScreenState extends State { style: const TextStyle( fontFamily: 'Amiri', fontSize: 22, + fontWeight: FontWeight.w400, height: 1.8, ), ), @@ -287,7 +296,8 @@ class _QuranBookmarksScreenState extends State { SizedBox( width: double.infinity, child: FilledButton.icon( - onPressed: () => context.push('/tools/quran/${bookmark.surahId}?startVerse=${bookmark.verseId}'), + onPressed: () => + context.push(_readingRoute(bookmark.surahId, bookmark.verseId)), icon: const Icon(LucideIcons.bookOpen, size: 18), label: const Text('Lanjutkan Membaca'), style: FilledButton.styleFrom( diff --git a/lib/features/quran/presentation/quran_enrichment_screen.dart b/lib/features/quran/presentation/quran_enrichment_screen.dart new file mode 100644 index 0000000..215f21d --- /dev/null +++ b/lib/features/quran/presentation/quran_enrichment_screen.dart @@ -0,0 +1,773 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../../app/theme/app_colors.dart'; +import '../../../data/services/muslim_api_service.dart'; + +class QuranEnrichmentScreen extends StatefulWidget { + const QuranEnrichmentScreen({super.key}); + + @override + State createState() => _QuranEnrichmentScreenState(); +} + +class _QuranEnrichmentScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + final TextEditingController _searchController = TextEditingController(); + final TextEditingController _pageController = TextEditingController(text: '1'); + + List> _surahs = []; + List> _searchResults = []; + List> _tafsirItems = []; + List> _asbabItems = []; + List> _juzItems = []; + List> _pageItems = []; + List> _themeItems = []; + List> _asmaItems = []; + + int _selectedSurahId = 1; + int _selectedPage = 1; + bool _loadingInit = true; + bool _loadingSearch = false; + bool _loadingTafsir = false; + bool _loadingAsbab = false; + bool _loadingPage = false; + String? _error; + + final Set _expandedWordByWord = {}; + final Map>> _wordByWord = {}; + final Set _loadingWordByWord = {}; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 7, vsync: this); + _bootstrap(); + } + + @override + void dispose() { + _tabController.dispose(); + _searchController.dispose(); + _pageController.dispose(); + super.dispose(); + } + + Future _bootstrap() async { + setState(() { + _loadingInit = true; + _error = null; + }); + + try { + final surahs = await MuslimApiService.instance.getAllSurahs(); + final juz = await MuslimApiService.instance.getJuzList(); + final themes = await MuslimApiService.instance.getThemes(); + final asma = await MuslimApiService.instance.getAsmaulHusna(); + + if (!mounted) return; + setState(() { + _surahs = surahs; + _selectedSurahId = surahs.isNotEmpty + ? ((surahs.first['nomor'] as int?) ?? 1) + : 1; + _juzItems = juz; + _themeItems = themes; + _asmaItems = asma; + _loadingInit = false; + }); + + await _loadTafsirForSelectedSurah(); + await _loadAsbabForSelectedSurah(); + await _loadPageAyah(); + } catch (_) { + if (!mounted) return; + setState(() { + _loadingInit = false; + _error = 'Gagal memuat data enrichment'; + }); + } + } + + Future _runSearch() async { + final query = _searchController.text.trim(); + if (query.isEmpty) { + setState(() => _searchResults = []); + return; + } + + setState(() => _loadingSearch = true); + final result = await MuslimApiService.instance.searchAyah(query); + if (!mounted) return; + setState(() { + _searchResults = result; + _loadingSearch = false; + }); + } + + Future _loadTafsirForSelectedSurah() async { + setState(() => _loadingTafsir = true); + final result = + await MuslimApiService.instance.getTafsirBySurah(_selectedSurahId); + if (!mounted) return; + setState(() { + _tafsirItems = result; + _loadingTafsir = false; + }); + } + + Future _loadAsbabForSelectedSurah() async { + setState(() => _loadingAsbab = true); + final result = await MuslimApiService.instance.getAsbabBySurah(_selectedSurahId); + if (!mounted) return; + setState(() { + _asbabItems = result; + _loadingAsbab = false; + }); + } + + Future _loadPageAyah() async { + setState(() => _loadingPage = true); + final page = int.tryParse(_pageController.text.trim()) ?? _selectedPage; + final safePage = page.clamp(1, 604); + final result = await MuslimApiService.instance.getAyahByPage(safePage); + if (!mounted) return; + setState(() { + _selectedPage = safePage; + _pageController.text = '$safePage'; + _pageItems = result; + _loadingPage = false; + }); + } + + Future _toggleWordByWord(Map ayah) async { + final surah = (ayah['surah'] as num?)?.toInt(); + final ayahNum = (ayah['ayah'] as num?)?.toInt(); + if (surah == null || ayahNum == null) return; + + final key = '$surah:$ayahNum'; + final expanded = _expandedWordByWord.contains(key); + + if (expanded) { + setState(() => _expandedWordByWord.remove(key)); + return; + } + + if (_wordByWord.containsKey(key)) { + setState(() => _expandedWordByWord.add(key)); + return; + } + + setState(() { + _loadingWordByWord.add(key); + _expandedWordByWord.add(key); + }); + + final words = await MuslimApiService.instance.getWordByWord(surah, ayahNum); + if (!mounted) return; + setState(() { + _wordByWord[key] = words; + _loadingWordByWord.remove(key); + }); + } + + String _surahNameById(int surahId) { + for (final s in _surahs) { + if (s['nomor'] == surahId) { + return s['namaLatin']?.toString() ?? 'Surah $surahId'; + } + } + return 'Surah $surahId'; + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('Quran Enrichment'), + actions: [ + IconButton( + onPressed: _bootstrap, + icon: const Icon(LucideIcons.refreshCw), + tooltip: 'Muat ulang', + ), + ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: AppColors.primary, + unselectedLabelColor: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + indicatorColor: AppColors.primary, + tabs: const [ + Tab(text: 'Cari'), + Tab(text: 'Tafsir'), + Tab(text: 'Asbab'), + Tab(text: 'Juz'), + Tab(text: 'Halaman'), + Tab(text: 'Tema'), + Tab(text: 'Asmaul Husna'), + ], + ), + ), + body: _loadingInit + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Text( + _error!, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : TabBarView( + controller: _tabController, + children: [ + _buildSearchTab(context, isDark), + _buildTafsirTab(context, isDark), + _buildAsbabTab(context, isDark), + _buildJuzTab(context, isDark), + _buildPageTab(context, isDark), + _buildThemeTab(context, isDark), + _buildAsmaTab(context, isDark), + ], + ), + ); + } + + Widget _buildSearchTab(BuildContext context, bool isDark) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _runSearch(), + decoration: InputDecoration( + hintText: 'Cari ayat, tema, atau kata kunci...', + prefixIcon: const Icon(LucideIcons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _runSearch, + child: const Text('Cari'), + ), + ], + ), + ), + Expanded( + child: _loadingSearch + ? const Center(child: CircularProgressIndicator()) + : _searchResults.isEmpty + ? Center( + child: Text( + 'Belum ada hasil pencarian', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final ayah = _searchResults[index]; + final surahId = (ayah['surah'] as num?)?.toInt() ?? 0; + final ayahNum = (ayah['ayah'] as num?)?.toInt() ?? 0; + final key = '$surahId:$ayahNum'; + final expanded = _expandedWordByWord.contains(key); + final words = _wordByWord[key] ?? const []; + final loadingWords = _loadingWordByWord.contains(key); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark + ? AppColors.surfaceDark + : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '${_surahNameById(surahId)} : $ayahNum', + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + TextButton.icon( + onPressed: () => _toggleWordByWord(ayah), + icon: Icon( + expanded + ? LucideIcons.chevronUp + : LucideIcons.languages, + size: 16, + ), + label: Text( + expanded ? 'Tutup' : 'Per Kata', + ), + ), + ], + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Text( + ayah['arab']?.toString() ?? '', + textAlign: TextAlign.right, + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 24, + fontWeight: FontWeight.w400, + height: 1.8, + ), + ), + ), + const SizedBox(height: 8), + Text( + ayah['text']?.toString() ?? '', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + if (expanded) ...[ + const SizedBox(height: 12), + if (loadingWords) + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Center( + child: CircularProgressIndicator(), + ), + ) + else if (words.isEmpty) + Text( + 'Data kata tidak tersedia untuk ayat ini.', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: words.map((word) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.primary + .withValues(alpha: 0.08), + borderRadius: + BorderRadius.circular(10), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + word['arab']?.toString() ?? '', + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 18, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 2), + Text( + word['word']?.toString() ?? '', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + const SizedBox(height: 2), + Text( + word['indo']?.toString() ?? '', + style: TextStyle( + fontSize: 11, + color: isDark + ? AppColors + .textSecondaryDark + : AppColors + .textSecondaryLight, + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ], + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildTafsirTab(BuildContext context, bool isDark) { + return Column( + children: [ + _buildSurahSelector( + onChanged: (value) { + setState(() => _selectedSurahId = value); + _loadTafsirForSelectedSurah(); + }, + ), + Expanded( + child: _loadingTafsir + ? const Center(child: CircularProgressIndicator()) + : _tafsirItems.isEmpty + ? _emptyText(isDark, 'Belum ada data tafsir untuk surah ini') + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + itemCount: _tafsirItems.length, + itemBuilder: (context, index) { + final item = _tafsirItems[index]; + final ayah = item['nomorAyat']?.toString() ?? '-'; + final wajiz = item['wajiz']?.toString() ?? ''; + final tahlili = item['tahlili']?.toString() ?? ''; + return _buildCard( + isDark, + title: 'Ayat $ayah', + body: '$wajiz\n\n$tahlili', + ); + }, + ), + ), + ], + ); + } + + Widget _buildAsbabTab(BuildContext context, bool isDark) { + return Column( + children: [ + _buildSurahSelector( + onChanged: (value) { + setState(() => _selectedSurahId = value); + _loadAsbabForSelectedSurah(); + }, + ), + Expanded( + child: _loadingAsbab + ? const Center(child: CircularProgressIndicator()) + : _asbabItems.isEmpty + ? _emptyText( + isDark, + 'Belum ada data asbabun nuzul untuk surah ini', + ) + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + itemCount: _asbabItems.length, + itemBuilder: (context, index) { + final item = _asbabItems[index]; + final ayah = item['nomorAyat']?.toString() ?? '-'; + return _buildCard( + isDark, + title: 'Ayat $ayah', + body: item['text']?.toString() ?? '', + ); + }, + ), + ), + ], + ); + } + + Widget _buildJuzTab(BuildContext context, bool isDark) { + if (_juzItems.isEmpty) { + return _emptyText(isDark, 'Data juz tidak tersedia'); + } + + return ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + itemCount: _juzItems.length, + itemBuilder: (context, index) { + final item = _juzItems[index]; + final number = item['number']?.toString() ?? '-'; + final startName = item['name_start_id']?.toString() ?? '-'; + final endName = item['name_end_id']?.toString() ?? '-'; + final startVerse = item['verse_start']?.toString() ?? '-'; + final endVerse = item['verse_end']?.toString() ?? '-'; + + return _buildCard( + isDark, + title: 'Juz $number', + body: + 'Mulai: $startName ayat $startVerse\nSelesai: $endName ayat $endVerse', + ); + }, + ); + } + + Widget _buildPageTab(BuildContext context, bool isDark) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _pageController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Nomor Halaman (1-604)', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _loadPageAyah, + child: const Text('Tampilkan'), + ), + ], + ), + ), + Expanded( + child: _loadingPage + ? const Center(child: CircularProgressIndicator()) + : _pageItems.isEmpty + ? _emptyText(isDark, 'Tidak ada data untuk halaman ini') + : ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + itemCount: _pageItems.length, + itemBuilder: (context, index) { + final item = _pageItems[index]; + final surahId = (item['surah'] as num?)?.toInt() ?? 0; + final ayah = item['ayah']?.toString() ?? '-'; + + return _buildCard( + isDark, + title: '${_surahNameById(surahId)} : $ayah', + body: + '${item['arab']?.toString() ?? ''}\n\n${item['text']?.toString() ?? ''}', + ); + }, + ), + ), + ], + ); + } + + Widget _buildThemeTab(BuildContext context, bool isDark) { + if (_themeItems.isEmpty) { + return _emptyText(isDark, 'Data tema belum tersedia'); + } + + return ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + itemCount: _themeItems.length, + itemBuilder: (context, index) { + final item = _themeItems[index]; + return _buildCard( + isDark, + title: 'Tema #${item['id'] ?? '-'}', + body: item['name']?.toString() ?? '', + ); + }, + ); + } + + Widget _buildAsmaTab(BuildContext context, bool isDark) { + if (_asmaItems.isEmpty) { + return _emptyText(isDark, 'Data Asmaul Husna tidak tersedia'); + } + + return ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + itemCount: _asmaItems.length, + itemBuilder: (context, index) { + final item = _asmaItems[index]; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${item['id'] ?? '-'}', + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['arab']?.toString() ?? '', + style: const TextStyle( + fontFamily: 'Amiri', + fontSize: 22, + fontWeight: FontWeight.w400, + ), + ), + Text( + item['latin']?.toString() ?? '', + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + const SizedBox(height: 2), + Text( + item['indo']?.toString() ?? '', + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildSurahSelector({required ValueChanged onChanged}) { + if (_surahs.isEmpty) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: _emptyText( + Theme.of(context).brightness == Brightness.dark, + 'Data surah tidak tersedia', + ), + ); + } + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: AppColors.primary.withValues(alpha: 0.2)), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedSurahId, + isExpanded: true, + items: _surahs.map((surah) { + final id = (surah['nomor'] as num?)?.toInt() ?? 1; + final name = surah['namaLatin']?.toString() ?? 'Surah $id'; + return DropdownMenuItem( + value: id, + child: Text('$id. $name'), + ); + }).toList(), + onChanged: (value) { + if (value == null) return; + onChanged(value); + }, + ), + ), + ), + ); + } + + Widget _buildCard(bool isDark, {required String title, required String body}) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.1) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.primary, + ), + ), + const SizedBox(height: 8), + Text(body, style: const TextStyle(height: 1.5)), + ], + ), + ); + } + + Widget _emptyText(bool isDark, String text) { + return Center( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ); + } +} diff --git a/lib/features/quran/presentation/quran_murattal_screen.dart b/lib/features/quran/presentation/quran_murattal_screen.dart index 5a3a1a7..1067af4 100644 --- a/lib/features/quran/presentation/quran_murattal_screen.dart +++ b/lib/features/quran/presentation/quran_murattal_screen.dart @@ -9,14 +9,11 @@ import 'package:lucide_icons/lucide_icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app/theme/app_colors.dart'; -import '../../../data/services/equran_service.dart'; +import '../../../data/services/muslim_api_service.dart'; import '../../../data/services/unsplash_service.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import '../../../data/local/hive_boxes.dart'; -import '../../../data/local/models/app_settings.dart'; /// Quran Murattal (audio player) screen. -/// Implements full Surah playback using just_audio and EQuran v2 API. +/// Implements full Surah playback using just_audio. class QuranMurattalScreen extends ConsumerStatefulWidget { final String surahId; final String? initialQariId; @@ -77,7 +74,7 @@ class _QuranMurattalScreenState extends ConsumerState { Future _initDataAndPlayer() async { final surahNum = int.tryParse(widget.surahId) ?? 1; - final data = await EQuranService.instance.getSurah(surahNum); + final data = await MuslimApiService.instance.getSurah(surahNum); if (data != null && mounted) { setState(() { @@ -186,7 +183,10 @@ class _QuranMurattalScreenState extends ConsumerState { void _navigateToSurahNumber(int surahNum, {bool autoplay = false}) { if (surahNum >= 1 && surahNum <= 114) { - context.pushReplacement('/tools/quran/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay'); + final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran'; + context.pushReplacement( + '$base/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay', + ); } } @@ -219,7 +219,7 @@ class _QuranMurattalScreenState extends ConsumerState { ), ), const SizedBox(height: 16), - ...EQuranService.qariNames.entries.map((entry) { + ...MuslimApiService.qariNames.entries.map((entry) { final isSelected = entry.key == _selectedQariId; return ListTile( leading: Icon( @@ -287,7 +287,7 @@ class _QuranMurattalScreenState extends ConsumerState { const SizedBox(height: 8), Expanded( child: FutureBuilder>>( - future: EQuranService.instance.getAllSurahs(), + future: MuslimApiService.instance.getAllSurahs(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); @@ -339,7 +339,7 @@ class _QuranMurattalScreenState extends ConsumerState { Navigator.pop(context); if (!isCurrentSurah) { context.pushReplacement( - '/tools/quran/$surahNum/murattal?qariId=$_selectedQariId', + '${widget.isSimpleModeTab ? '/quran' : '/tools/quran'}/$surahNum/murattal?qariId=$_selectedQariId', ); } }, @@ -360,8 +360,6 @@ class _QuranMurattalScreenState extends ConsumerState { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - final box = Hive.box(HiveBoxes.settings); - final isSimpleMode = box.get('default')?.simpleMode ?? false; final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}'; final hasPhoto = _unsplashPhoto != null; @@ -519,7 +517,7 @@ class _QuranMurattalScreenState extends ConsumerState { const SizedBox(height: 32), // Qari name Text( - EQuranService.qariNames[_selectedQariId] ?? 'Memuat...', + MuslimApiService.qariNames[_selectedQariId] ?? 'Memuat...', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, @@ -742,7 +740,7 @@ class _QuranMurattalScreenState extends ConsumerState { color: _unsplashPhoto != null ? Colors.white : AppColors.primary), const SizedBox(width: 8), Text( - EQuranService.qariNames[_selectedQariId] ?? 'Ganti Qari', + MuslimApiService.qariNames[_selectedQariId] ?? 'Ganti Qari', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, diff --git a/lib/features/quran/presentation/quran_reading_screen.dart b/lib/features/quran/presentation/quran_reading_screen.dart index 972b344..81bc3f0 100644 --- a/lib/features/quran/presentation/quran_reading_screen.dart +++ b/lib/features/quran/presentation/quran_reading_screen.dart @@ -12,7 +12,7 @@ import '../../../data/local/models/quran_bookmark.dart'; import '../../../data/local/models/app_settings.dart'; import '../../../data/local/models/daily_worship_log.dart'; import '../../../data/local/models/tilawah_log.dart'; -import '../../../data/services/equran_service.dart'; +import '../../../data/services/muslim_api_service.dart'; import '../../../core/providers/tilawah_tracking_provider.dart'; class QuranReadingScreen extends ConsumerStatefulWidget { @@ -151,7 +151,8 @@ class _QuranReadingScreenState extends ConsumerState { Future _loadSurah() async { final surahNum = int.tryParse(widget.surahId) ?? 1; - final data = await EQuranService.instance.getSurah(surahNum); + final data = await MuslimApiService.instance.getSurah(surahNum); + if (!mounted) return; if (data != null) { setState(() { _surah = data; @@ -356,7 +357,9 @@ class _QuranReadingScreenState extends ConsumerState { ), ); } - } Future _showEndTrackingDialog(TilawahSession session, int endVerseId) async { + } + + Future _showEndTrackingDialog(TilawahSession session, int endVerseId) async { final endSurahId = _surah!['nomor'] ?? 1; final endSurahName = _surah!['namaLatin'] ?? ''; @@ -367,26 +370,30 @@ class _QuranReadingScreenState extends ConsumerState { calculatedAyat = (endVerseId - session.startVerseId).abs() + 1; } else { // Cross surah calculation - final allSurahs = await EQuranService.instance.getAllSurahs(); + final allSurahs = await MuslimApiService.instance.getAllSurahs(); if (allSurahs.isNotEmpty) { int startSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == session.startSurahId); int endSurahIdx = allSurahs.indexWhere((s) => s['nomor'] == endSurahId); - - // Ensure chronological calculation - if (startSurahIdx > endSurahIdx) { - final tempIdx = startSurahIdx; startSurahIdx = endSurahIdx; endSurahIdx = tempIdx; + + if (startSurahIdx < 0 || endSurahIdx < 0) { + calculatedAyat = (endVerseId - session.startVerseId).abs() + 1; + } else { + // Ensure chronological calculation + if (startSurahIdx > endSurahIdx) { + final tempIdx = startSurahIdx; startSurahIdx = endSurahIdx; endSurahIdx = tempIdx; + } + + final startSurahData = allSurahs[startSurahIdx]; + final int totalAyatInStart = (startSurahData['jumlahAyat'] as num?)?.toInt() ?? 1; + + calculatedAyat += (totalAyatInStart - session.startVerseId) + 1; // Ayats inside StartSurah + + for (int i = startSurahIdx + 1; i < endSurahIdx; i++) { + calculatedAyat += (allSurahs[i]['jumlahAyat'] as int? ?? 0); // Intermediate Surahs + } + + calculatedAyat += endVerseId; // Ayats inside EndSurah } - - final startSurahData = allSurahs[startSurahIdx]; - final int totalAyatInStart = (startSurahData['jumlahAyat'] as num?)?.toInt() ?? 1; - - calculatedAyat += (totalAyatInStart - session.startVerseId) + 1; // Ayats inside StartSurah - - for (int i = startSurahIdx + 1; i < endSurahIdx; i++) { - calculatedAyat += (allSurahs[i]['jumlahAyat'] as int? ?? 0); // Intermediate Surahs - } - - calculatedAyat += endVerseId; // Ayats inside EndSurah } else { calculatedAyat = 1; // Fallback } @@ -572,6 +579,11 @@ class _QuranReadingScreenState extends ConsumerState { ], ), actions: [ + IconButton( + icon: const Icon(LucideIcons.headphones), + tooltip: 'Murattal Surah', + onPressed: _navigateToMurattal, + ), IconButton( icon: Icon( LucideIcons.brain, @@ -620,6 +632,7 @@ class _QuranReadingScreenState extends ConsumerState { style: TextStyle( fontFamily: 'Amiri', fontSize: 26, + fontWeight: FontWeight.w400, ), ), const SizedBox(height: 4), @@ -802,6 +815,7 @@ class _QuranReadingScreenState extends ConsumerState { style: const TextStyle( fontFamily: 'Amiri', fontSize: 26, + fontWeight: FontWeight.w400, height: 2.0, ), ), diff --git a/lib/features/quran/presentation/quran_screen.dart b/lib/features/quran/presentation/quran_screen.dart index 321f2b0..4e829b9 100644 --- a/lib/features/quran/presentation/quran_screen.dart +++ b/lib/features/quran/presentation/quran_screen.dart @@ -7,7 +7,7 @@ import '../../../app/theme/app_colors.dart'; import '../../../data/local/hive_boxes.dart'; import '../../../data/local/models/app_settings.dart'; import '../../../data/local/models/quran_bookmark.dart'; -import '../../../data/services/equran_service.dart'; +import '../../../data/services/muslim_api_service.dart'; class QuranScreen extends ConsumerStatefulWidget { final bool isSimpleModeTab; @@ -36,7 +36,8 @@ class _QuranScreenState extends ConsumerState { } Future _loadSurahs() async { - final data = await EQuranService.instance.getAllSurahs(); + final data = await MuslimApiService.instance.getAllSurahs(); + if (!mounted) return; setState(() { _surahs = data; _loading = false; @@ -100,8 +101,6 @@ class _QuranScreenState extends ConsumerState { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - final box = Hive.box(HiveBoxes.settings); - final isSimpleMode = box.get('default')?.simpleMode ?? false; final filtered = _searchQuery.isEmpty ? _surahs : _surahs @@ -119,7 +118,15 @@ class _QuranScreenState extends ConsumerState { actions: [ IconButton( icon: const Icon(LucideIcons.bookmark), - onPressed: () => context.push('/tools/quran/bookmarks'), + onPressed: () => context.push(widget.isSimpleModeTab + ? '/quran/bookmarks' + : '/tools/quran/bookmarks'), + ), + IconButton( + icon: const Icon(LucideIcons.sparkles), + onPressed: () => context.push(widget.isSimpleModeTab + ? '/quran/enrichment' + : '/tools/quran/enrichment'), ), IconButton( icon: const Icon(LucideIcons.settings2), @@ -198,8 +205,9 @@ class _QuranScreenState extends ConsumerState { final hasLastRead = box.values.any((b) => b.isLastRead && b.surahId == number); return ListTile( - onTap: () => - context.push('/tools/quran/$number'), + onTap: () => context.push(widget.isSimpleModeTab + ? '/quran/$number' + : '/tools/quran/$number'), contentPadding: const EdgeInsets.symmetric( horizontal: 0, vertical: 6), leading: Container( @@ -250,6 +258,7 @@ class _QuranScreenState extends ConsumerState { style: const TextStyle( fontFamily: 'Amiri', fontSize: 18, + fontWeight: FontWeight.w400, ), ), ); diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 10114be..233ffc3 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -244,6 +244,70 @@ class _SettingsScreenState extends ConsumerState { ), const SizedBox(height: 24), + // ── DZIKIR DISPLAY ── + _sectionLabel('TAMPILAN DZIKIR'), + const SizedBox(height: 12), + _buildSegmentSettingCard( + isDark, + title: 'Mode Tampilan Dzikir', + subtitle: 'Pilih daftar baris atau fokus per slide', + value: _settings.dzikirDisplayMode, + options: const { + 'list': 'Daftar (Baris)', + 'focus': 'Fokus (Slide)', + }, + onChanged: (value) { + _settings.dzikirDisplayMode = value; + _saveSettings(); + }, + ), + if (_settings.dzikirDisplayMode == 'focus') ...[ + const SizedBox(height: 10), + _buildSegmentSettingCard( + isDark, + title: 'Posisi Tombol Hitung', + subtitle: 'Atur posisi tombol pada mode fokus', + value: _settings.dzikirCounterButtonPosition, + options: const { + 'bottomPill': 'Pill Bawah', + 'fabCircle': 'Bulat Kanan Bawah', + }, + onChanged: (value) { + _settings.dzikirCounterButtonPosition = value; + _saveSettings(); + }, + ), + const SizedBox(height: 10), + _settingRow( + isDark, + icon: LucideIcons.arrowRight, + iconColor: const Color(0xFF00B894), + title: 'Lanjut Otomatis Saat Target Tercapai', + trailing: IosToggle( + value: _settings.dzikirAutoAdvance, + onChanged: (v) { + _settings.dzikirAutoAdvance = v; + _saveSettings(); + }, + ), + ), + ], + const SizedBox(height: 10), + _settingRow( + isDark, + icon: LucideIcons.vibrate, + iconColor: const Color(0xFF6C5CE7), + title: 'Getaran Saat Hitung', + trailing: IosToggle( + value: _settings.dzikirHapticOnCount, + onChanged: (v) { + _settings.dzikirHapticOnCount = v; + _saveSettings(); + }, + ), + ), + const SizedBox(height: 24), + // ── PRAYER SETTINGS ── _sectionLabel('WAKTU SHOLAT'), const SizedBox(height: 12), @@ -438,6 +502,103 @@ class _SettingsScreenState extends ConsumerState { ); } + Widget _buildSegmentSettingCard( + bool isDark, { + required String title, + String? subtitle, + required String value, + required Map options, + required ValueChanged onChanged, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.08) + : AppColors.cream, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 12, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), + ), + ], + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isDark + ? AppColors.backgroundDark + : AppColors.backgroundLight, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? AppColors.primary.withValues(alpha: 0.08) + : AppColors.cream, + ), + ), + child: Row( + children: options.entries.map((entry) { + final selected = value == entry.key; + return Expanded( + child: GestureDetector( + onTap: () => onChanged(entry.key), + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 10, + ), + decoration: BoxDecoration( + color: selected + ? AppColors.primary + : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + entry.value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: selected + ? AppColors.onPrimary + : (isDark + ? AppColors.textPrimaryDark + : AppColors.textPrimaryLight), + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } + void _showMethodDialog(BuildContext context) { showDialog( context: context, diff --git a/lib/features/tools/presentation/tools_screen.dart b/lib/features/tools/presentation/tools_screen.dart index 7e6c2c8..1733335 100644 --- a/lib/features/tools/presentation/tools_screen.dart +++ b/lib/features/tools/presentation/tools_screen.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../app/theme/app_colors.dart'; import '../../../core/widgets/tool_card.dart'; -import '../../../data/services/equran_service.dart'; +import '../../../data/services/muslim_api_service.dart'; class ToolsScreen extends ConsumerWidget { const ToolsScreen({super.key}); @@ -29,7 +29,7 @@ class ToolsScreen extends ConsumerWidget { const SizedBox(width: 8), ], ), - body: Padding( + body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -50,9 +50,9 @@ class ToolsScreen extends ConsumerWidget { child: ToolCard( icon: LucideIcons.bookOpen, title: 'Al-Quran\nTerjemahan', - color: const Color(0xFF00b894), + color: const Color(0xFF00B894), isDark: isDark, - onTap: () => context.push('/quran'), + onTap: () => context.push('/tools/quran'), ), ), const SizedBox(width: 12), @@ -62,7 +62,7 @@ class ToolsScreen extends ConsumerWidget { title: 'Quran\nMurattal', color: const Color(0xFF7B61FF), isDark: isDark, - onTap: () => context.push('/quran/1/murattal'), + onTap: () => context.push('/tools/quran/1/murattal'), ), ), ], @@ -83,25 +83,65 @@ class ToolsScreen extends ConsumerWidget { Expanded( child: ToolCard( icon: LucideIcons.sparkles, - title: 'Tasbih\nDigital', + title: 'Dzikir\nHarian', color: AppColors.primary, isDark: isDark, - onTap: () => context.push('/dzikir'), + onTap: () => context.push('/tools/dzikir'), ), ), ], ), - const SizedBox(height: 32), - // Ayat Hari Ini + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ToolCard( + icon: LucideIcons.heart, + title: 'Kumpulan\nDoa', + color: const Color(0xFFE17055), + isDark: isDark, + onTap: () => context.push('/tools/doa'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ToolCard( + icon: LucideIcons.library, + title: "Hadits\nArba'in", + color: const Color(0xFF6C5CE7), + isDark: isDark, + onTap: () => context.push('/tools/hadits'), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ToolCard( + icon: LucideIcons.sparkles, + title: 'Quran\nEnrichment', + color: const Color(0xFF00CEC9), + isDark: isDark, + onTap: () => context.push('/tools/quran/enrichment'), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + const SizedBox(height: 28), FutureBuilder?>( - future: EQuranService.instance.getDailyAyat(), + future: MuslimApiService.instance.getDailyAyat(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: isDark ? AppColors.primary.withValues(alpha: 0.08) : const Color(0xFFF5F9F0), + color: isDark + ? AppColors.primary.withValues(alpha: 0.08) + : const Color(0xFFF5F9F0), borderRadius: BorderRadius.circular(16), ), child: const Center(child: CircularProgressIndicator()), @@ -109,7 +149,7 @@ class ToolsScreen extends ConsumerWidget { } if (!snapshot.hasData || snapshot.data == null) { - return const SizedBox.shrink(); // Hide if error/no internet + return const SizedBox.shrink(); } final data = snapshot.data!; @@ -117,7 +157,9 @@ class ToolsScreen extends ConsumerWidget { width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: isDark ? AppColors.primary.withValues(alpha: 0.08) : const Color(0xFFF5F9F0), + color: isDark + ? AppColors.primary.withValues(alpha: 0.08) + : const Color(0xFFF5F9F0), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -131,13 +173,19 @@ class ToolsScreen extends ConsumerWidget { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, ), ), IconButton( - icon: Icon(LucideIcons.share2, - size: 18, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), + icon: Icon( + LucideIcons.share2, + size: 18, + color: isDark + ? AppColors.textSecondaryDark + : AppColors.textSecondaryLight, + ), onPressed: () {}, ), ], @@ -150,6 +198,7 @@ class ToolsScreen extends ConsumerWidget { style: const TextStyle( fontFamily: 'Amiri', fontSize: 24, + fontWeight: FontWeight.w400, height: 1.8, ), textAlign: TextAlign.right, diff --git a/logo-luxury-theme-brief.md b/logo-luxury-theme-brief.md new file mode 100644 index 0000000..d2507bb --- /dev/null +++ b/logo-luxury-theme-brief.md @@ -0,0 +1,107 @@ +# Logo Palette + Luxury Active Menu Brief + +## 1) Objective +Adopt the app's visual identity from the logo palette (teal + gold) and introduce a luxury-feel active menu state for both dark and light themes. + +Key intent: +- Brand-consistent color system. +- Premium, elegant active menu treatment. +- Subtle animated gold shine (not flashy, not distracting). + +## 2) Design Direction + +### Brand mood +- Calm spiritual base: teal tones. +- Premium emphasis: gold used only for highlights/active states. +- Minimal and clean surfaces, with controlled depth (soft shadow + thin stroke). + +### Usage rule +- Teal = foundation color. +- Gold = premium interaction color (active nav, selected/high-priority accents). +- Avoid using gold as the global background. + +## 3) Proposed Color Tokens + +These are starting values to tune after visual QA: + +### Core brand tokens +- `brand.teal.500`: `#118A8D` +- `brand.teal.700`: `#0C676A` +- `brand.teal.900`: `#0A4447` +- `brand.gold.400`: `#D6A21D` +- `brand.gold.300`: `#E9C75B` +- `brand.gold.200`: `#F6DE96` +- `brand.gold.700`: `#8B6415` + +### Dark theme base +- `bg.dark`: `#0F1217` +- `surface.dark`: `#171B22` +- `surface.dark.elevated`: `#1D222B` +- `text.dark.primary`: `#E8ECF2` +- `text.dark.secondary`: `#9AA4B2` + +### Light theme base +- `bg.light`: `#F3F4F6` +- `surface.light`: `#FFFFFF` +- `surface.light.elevated`: `#F9FAFB` +- `text.light.primary`: `#1F2937` +- `text.light.secondary`: `#6B7280` + +## 4) Active Menu Visual Spec + +### Shape and structure +- Active menu item uses rounded-square container. +- Ring consists of: + - Inner metallic gold stroke. + - Soft outer glow (stronger in dark mode, lighter in light mode). +- Inactive items remain neutral gray/secondary text. + +### Gold ring treatment +- Gradient on ring: dark-gold -> bright-gold -> pale-gold -> dark-gold. +- Keep ring thin and crisp (avoid thick glowing halo). +- Shine highlight should pass around the ring edge only. + +### Light mode adaptation +- Reduce glow blur and opacity by ~35-45% vs dark mode. +- Add subtle neutral shadow to preserve depth on bright surfaces. + +## 5) Motion Spec (Shine Animation) + +- Animation target: active menu ring only. +- Loop duration: `2.8s-3.6s`. +- Shine pass visibility: short burst (`~700-900ms`) then calm period. +- Easing: smooth in/out (no linear harsh movement). +- Keep animation subtle enough to avoid drawing attention from content. + +Reduced motion behavior: +- If reduced motion is enabled, disable moving shine and keep static gold ring. + +## 6) Accessibility & Quality Constraints + +- Maintain icon/text contrast minimum: + - Non-text UI/icon target: at least `3:1`. + - Small text target: at least `4.5:1`. +- Gold accents must not reduce legibility. +- Active state must remain distinguishable in both themes without relying only on color. + +## 7) Product Rules + +- Apply luxury active style only to active navigation state (not every component). +- Keep one source of truth for tokens across screens. +- Preserve current interaction speed and perceived responsiveness. + +## 8) Rollout Plan + +1. Approve token palette on static mockups (dark + light). +2. Apply to one pilot component: bottom navigation active item. +3. Validate contrast and motion comfort. +4. Roll out to other selected active states (tabs/chips), not all controls. + +## 9) Acceptance Criteria + +- Brand identity clearly reflects logo colors. +- Active menu looks premium in dark and light mode. +- Shine animation feels elegant and subtle, never distracting. +- No readability regressions. +- Reduced-motion path is supported. + diff --git a/macos/Podfile b/macos/Podfile index ff5ddb3..42c2b85 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -38,5 +38,16 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' + if target.name == 'audio_session' + warning_flags = config.build_settings['WARNING_CFLAGS'] || '$(inherited)' + warning_flags = [warning_flags] unless warning_flags.is_a?(Array) + unless warning_flags.include?('-Wno-unused-value') + warning_flags << '-Wno-unused-value' + end + config.build_settings['WARNING_CFLAGS'] = warning_flags + end + end end end diff --git a/macos/Podfile.lock b/macos/Podfile.lock index db3d2cf..431a023 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -53,16 +53,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910 - audio_session: 728ae3823d914f809c485d390274861a24b0904e - flutter_local_notifications: 14e285ca39907db50704f7f46c9ab7a526bd7ead + audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd + audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e + flutter_local_notifications: 1fc7ffb10a83d6a2eeeeddb152d43f1944b0aad0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd - just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 +PODFILE CHECKSUM: e84c52ef5d3a8e77f70c2a1d22c490d3e6258427 -COCOAPODS: 1.12.0 +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index ffe8bd3..d41ee1a 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 0C90C3ED62E3E14394A23EE5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - DDE68F59044EBC73D03E0962 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */; }; + 41981E14C9DB7600396CF65B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0696C611C50F37ED234934AD /* Pods_Runner.framework */; }; + E4654D13A28FD9D97A9BEAB5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AEFCC482F4CDCD13DE76DF7 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,11 +62,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 00FDE0E819DF753D953FEBB2 /* 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 = ""; }; - 06DAA92E91CF7851957A0E28 /* 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 = ""; }; - 07D5D0934671F750DA630F1D /* 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 = ""; }; - 0BB6B1FDF75FA1C8F8165DEB /* 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 = ""; }; - 1F212DF96DD0BB5851DDFC62 /* 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 = ""; }; + 0696C611C50F37ED234934AD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0EA81656FD366AC44330F725 /* 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 = ""; }; + 2177AABE95008D4FE1E5242D /* 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 = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -83,11 +81,13 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AA0251CD9D710A50D527654 /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - E6FD2B4E523D8881848DBBE0 /* 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 = ""; }; + 9AEFCC482F4CDCD13DE76DF7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A59D8BEEC1995674B843D406 /* 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 = ""; }; + CEFABEB8B68EDF9F2E4321B2 /* 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 = ""; }; + F8D884B7ADEE919A7ABF960E /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DDE68F59044EBC73D03E0962 /* Pods_RunnerTests.framework in Frameworks */, + E4654D13A28FD9D97A9BEAB5 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,27 +103,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C90C3ED62E3E14394A23EE5 /* Pods_Runner.framework in Frameworks */, + 41981E14C9DB7600396CF65B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0534C72D1883289C9A7D2A97 /* Pods */ = { - isa = PBXGroup; - children = ( - 0BB6B1FDF75FA1C8F8165DEB /* Pods-Runner.debug.xcconfig */, - 07D5D0934671F750DA630F1D /* Pods-Runner.release.xcconfig */, - 00FDE0E819DF753D953FEBB2 /* Pods-Runner.profile.xcconfig */, - 1F212DF96DD0BB5851DDFC62 /* Pods-RunnerTests.debug.xcconfig */, - 06DAA92E91CF7851957A0E28 /* Pods-RunnerTests.release.xcconfig */, - E6FD2B4E523D8881848DBBE0 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -150,8 +136,8 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 0534C72D1883289C9A7D2A97 /* Pods */, + B1CD7E90020BCFEFA243FA5B /* Pods */, + C5836D92F95B6223EA2AB79E /* Frameworks */, ); sourceTree = ""; }; @@ -199,11 +185,25 @@ path = Runner; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { + B1CD7E90020BCFEFA243FA5B /* Pods */ = { isa = PBXGroup; children = ( - 6A4B57B5283BA4F62AC20241 /* Pods_Runner.framework */, - 7DF3757EFF54A1EC85BA5E22 /* Pods_RunnerTests.framework */, + A59D8BEEC1995674B843D406 /* Pods-Runner.debug.xcconfig */, + 2177AABE95008D4FE1E5242D /* Pods-Runner.release.xcconfig */, + F8D884B7ADEE919A7ABF960E /* Pods-Runner.profile.xcconfig */, + CEFABEB8B68EDF9F2E4321B2 /* Pods-RunnerTests.debug.xcconfig */, + 0EA81656FD366AC44330F725 /* Pods-RunnerTests.release.xcconfig */, + 4AA0251CD9D710A50D527654 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + C5836D92F95B6223EA2AB79E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0696C611C50F37ED234934AD /* Pods_Runner.framework */, + 9AEFCC482F4CDCD13DE76DF7 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -215,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 9B95A5421B9DABEF4DB6101B /* [CP] Check Pods Manifest.lock */, + CF2BEA357E30795FE78CD469 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -234,13 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 2DC954CD4D756DB6686B4570 /* [CP] Check Pods Manifest.lock */, + 379A5F95EEFA0001FF3150C5 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 7C73BC09BD737FBB6206AB8D /* [CP] Embed Pods Frameworks */, + 080782848EF12E763C1C7A22 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -323,26 +323,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 2DC954CD4D756DB6686B4570 /* [CP] Check Pods Manifest.lock */ = { + 080782848EF12E763C1C7A22 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - 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"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3399D490228B24CF009A79C7 /* ShellScript */ = { @@ -383,24 +378,29 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 7C73BC09BD737FBB6206AB8D /* [CP] Embed Pods Frameworks */ = { + 379A5F95EEFA0001FF3150C5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + 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"; showEnvVarsInLog = 0; }; - 9B95A5421B9DABEF4DB6101B /* [CP] Check Pods Manifest.lock */ = { + CF2BEA357E30795FE78CD469 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -473,7 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1F212DF96DD0BB5851DDFC62 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = CEFABEB8B68EDF9F2E4321B2 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -488,7 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 06DAA92E91CF7851957A0E28 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 0EA81656FD366AC44330F725 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -503,7 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E6FD2B4E523D8881848DBBE0 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 4AA0251CD9D710A50D527654 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d..00ec13b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eb..ba42d0b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa..82ac30a 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb5722..2e551a0 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318..d94908a 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e7..aef7509 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632c..d636ba6 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfe..521ae15 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 88cfd48..3efbab4 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d7..76d70aa 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c566..1e4ef4b 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ