feat: checkpoint API migration and dzikir UX updates

This commit is contained in:
Dwindi Ramadhana
2026-03-16 00:30:32 +07:00
parent c4696f2d9f
commit a049129a35
85 changed files with 4285 additions and 211 deletions

View File

@@ -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

View File

@@ -0,0 +1,5 @@
package com.jamshalat.jamshalat_diary
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -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

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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.

34
ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -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

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -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"

View File

@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

70
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Jamshalat Diary</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>jamshalat_diary</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@@ -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.
}
}

View File

@@ -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) ──

View File

@@ -64,7 +64,7 @@ class AppTextStyles {
static const TextStyle arabicLarge = TextStyle(
fontFamily: 'Amiri',
fontSize: 28,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w400,
height: 2.2,
);
}

View File

@@ -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,

View File

@@ -37,13 +37,17 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
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<AppSettings> {
..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

View File

@@ -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<String, String> 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<Map<String, dynamic>>? _surahListCache;
final Map<int, Map<String, dynamic>> _surahCache = {};
List<Map<String, dynamic>>? _allAyahCache;
List<Map<String, dynamic>>? _tafsirCache;
List<Map<String, dynamic>>? _asbabCache;
List<Map<String, dynamic>>? _juzCache;
List<Map<String, dynamic>>? _themeCache;
List<Map<String, dynamic>>? _asmaCache;
List<Map<String, dynamic>>? _doaCache;
List<Map<String, dynamic>>? _haditsCache;
final Map<String, List<Map<String, dynamic>>> _dzikirByTypeCache = {};
final Map<String, List<Map<String, dynamic>>> _wordByWordCache = {};
final Map<int, List<Map<String, dynamic>>> _pageAyahCache = {};
Future<dynamic> _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<String, dynamic>) {
return decoded['data'];
}
return null;
} catch (_) {
return null;
}
}
Future<dynamic> _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<String, dynamic>) {
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<String, dynamic> 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<String, String> _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<String, dynamic> _mapSurahSummary(Map<String, dynamic> 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<String, dynamic> _mapAyah(Map<String, dynamic> 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<List<Map<String, dynamic>>> getAllSurahs() async {
if (_surahListCache != null) return _surahListCache!;
final raw = await _getData('/v1/quran/surah');
if (raw is! List) return [];
_surahListCache = raw
.whereType<Map<String, dynamic>>()
.map(_mapSurahSummary)
.toList();
return _surahListCache!;
}
Future<Map<String, dynamic>?> getSurah(int number) async {
if (_surahCache.containsKey(number)) {
return _surahCache[number];
}
final surahs = await getAllSurahs();
Map<String, dynamic>? 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<String, dynamic>>()
.map(_mapAyah)
.toList();
final mapped = {
...summary,
'ayat': mappedAyah,
};
_surahCache[number] = mapped;
return mapped;
}
Future<Map<String, dynamic>?> 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<Map<String, dynamic>>.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<List<Map<String, dynamic>>> 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<String, dynamic>>().map((item) {
return {
'word': _asString(item['word']),
'arab': _asString(item['arab']),
'indo': _asString(item['indo']),
};
}).toList();
_wordByWordCache[key] = mapped;
return mapped;
}
Future<List<Map<String, dynamic>>> getAllAyah() async {
if (_allAyahCache != null) return _allAyahCache!;
final raw = await _getData('/v1/quran/ayah');
if (raw is! List) return [];
_allAyahCache = raw.whereType<Map<String, dynamic>>().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<List<Map<String, dynamic>>> getTafsirBySurah(int surahId) async {
if (_tafsirCache == null) {
final raw = await _getData('/v1/quran/tafsir');
if (raw is! List) return [];
_tafsirCache = raw.whereType<Map<String, dynamic>>().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 = <int, Map<String, dynamic>>{};
final ayahBySurahAyah = <String, Map<String, dynamic>>{};
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 = <Map<String, dynamic>>[];
for (final tafsir in _tafsirCache!) {
final tafsirId = _asInt(tafsir['id']);
final tafsirAyah = _asInt(tafsir['ayah']);
Map<String, dynamic>? 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<List<Map<String, dynamic>>> getAsbabBySurah(int surahId) async {
if (_asbabCache == null) {
final raw = await _getData('/v1/quran/asbab');
if (raw is! List) return [];
_asbabCache = raw.whereType<Map<String, dynamic>>().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 = <int, Map<String, dynamic>>{};
final ayahBySurahAyah = <String, Map<String, dynamic>>{};
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 = <Map<String, dynamic>>[];
for (final asbab in _asbabCache!) {
final asbabId = _asInt(asbab['id']);
final asbabAyah = _asInt(asbab['ayah']);
Map<String, dynamic>? 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<List<Map<String, dynamic>>> getJuzList() async {
if (_juzCache != null) return _juzCache!;
final raw = await _getData('/v1/quran/juz');
if (raw is! List) return [];
_juzCache = raw.whereType<Map<String, dynamic>>().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<List<Map<String, dynamic>>> 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<String, dynamic>>().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<List<Map<String, dynamic>>> getThemes() async {
if (_themeCache != null) return _themeCache!;
final raw = await _getData('/v1/quran/theme');
if (raw is! List) return [];
_themeCache = raw.whereType<Map<String, dynamic>>().map((item) {
return {
'id': _asInt(item['id']),
'name': _asString(item['name']),
};
}).toList();
return _themeCache!;
}
Future<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> getAsmaulHusna() async {
if (_asmaCache != null) return _asmaCache!;
final raw = await _getData('/v1/quran/asma');
if (raw is! List) return [];
_asmaCache = raw.whereType<Map<String, dynamic>>().map((item) {
return {
'id': _asInt(item['id']),
'arab': _asString(item['arab']),
'latin': _asString(item['latin']),
'indo': _asString(item['indo']),
};
}).toList();
return _asmaCache!;
}
Future<List<Map<String, dynamic>>> 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<String, dynamic>>().map((item) {
return {
'judul': _asString(item['judul']),
'arab': _asString(item['arab']),
'indo': _asString(item['indo']),
'source': _asString(item['source']),
};
}).toList();
return _doaCache!;
}
Future<List<Map<String, dynamic>>> 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<String, dynamic>>().map((item) {
return {
'no': _asInt(item['no']),
'judul': _asString(item['judul']),
'arab': _asString(item['arab']),
'indo': _asString(item['indo']),
};
}).toList();
return _haditsCache!;
}
Future<List<Map<String, dynamic>>> 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 = <Map<String, dynamic>>[];
for (var i = 0; i < raw.length; i++) {
final item = raw[i];
if (item is! Map<String, dynamic>) 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;
}
}

View File

@@ -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<DashboardScreen> {
),
],
),
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<AppSettings>(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<AppSettings>(HiveBoxes.settings)
.get('default')
?.simpleMode ??
false;
if (isSimple) {
context.push('/hadits');
} else {
context.push('/tools/hadits');
}
},
),
),
],
),
],
);
}
Widget _buildAyatHariIni(BuildContext context, bool isDark) {
return FutureBuilder<Map<String, dynamic>?>(
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<DashboardScreen> {
style: const TextStyle(
fontFamily: 'Amiri',
fontSize: 24,
fontWeight: FontWeight.w400,
height: 1.8,
),
textAlign: TextAlign.right,

View File

@@ -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<DoaScreen> createState() => _DoaScreenState();
}
class _DoaScreenState extends State<DoaScreen> {
final TextEditingController _searchController = TextEditingController();
List<Map<String, dynamic>> _allDoa = [];
List<Map<String, dynamic>> _filteredDoa = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadDoa();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _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,
),
),
],
],
),
);
},
),
),
],
),
);
}
}

View File

@@ -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<DzikirScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final Map<String, PageController> _pageControllers = {
'pagi': PageController(),
'petang': PageController(),
'solat': PageController(),
};
final Map<String, int> _focusPageIndex = {
'pagi': 0,
'petang': 0,
'solat': 0,
};
List<Map<String, dynamic>> _pagiItems = [];
List<Map<String, dynamic>> _petangItems = [];
List<Map<String, dynamic>> _sesudahSholatItems = [];
bool _loading = true;
String? _error;
late Box<DzikirCounter> _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<DzikirCounter>(HiveBoxes.dzikirCounters);
_todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
_loadData();
@@ -38,17 +60,68 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
@override
void dispose() {
_tabController.dispose();
for (final controller in _pageControllers.values) {
controller.dispose();
}
super.dispose();
}
Future<void> _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<Map<String, dynamic>>.from(json.decode(pagiJson));
_petangItems = List<Map<String, dynamic>>.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<DzikirScreen>
);
}
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<DzikirScreen>
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<AppSettings>(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<Box<AppSettings>>(
valueListenable:
Hive.box<AppSettings>(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<DzikirScreen>
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<Map<String, dynamic>> items, String prefix, String title, String subtitle) {
Widget _buildDzikirList(
BuildContext context,
bool isDark,
AppSettings settings,
List<Map<String, dynamic>> 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<DzikirScreen>
}
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<DzikirScreen>
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<DzikirScreen>
],
),
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<DzikirScreen>
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<DzikirScreen>
),
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<DzikirScreen>
},
);
}
Widget _buildFocusModeTab(
BuildContext context,
bool isDark,
AppSettings settings, {
required List<Map<String, dynamic>> 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<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic> 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,
),
],
),
),
);
}
}

View File

@@ -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<HaditsScreen> createState() => _HaditsScreenState();
}
class _HaditsScreenState extends State<HaditsScreen> {
final TextEditingController _searchController = TextEditingController();
List<Map<String, dynamic>> _allHadits = [];
List<Map<String, dynamic>> _filteredHadits = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadHadits();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _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,
),
),
],
),
);
},
),
),
],
),
);
}
}

View File

@@ -19,6 +19,14 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
bool _showLatin = true;
bool _showTerjemahan = true;
String _readingRoute(int surahId, int verseId) {
final isSimple =
Hive.box<AppSettings>(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<QuranBookmarksScreen> {
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<QuranBookmarksScreen> {
style: const TextStyle(
fontFamily: 'Amiri',
fontSize: 22,
fontWeight: FontWeight.w400,
height: 1.8,
),
),
@@ -287,7 +296,8 @@ class _QuranBookmarksScreenState extends State<QuranBookmarksScreen> {
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(

View File

@@ -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<QuranEnrichmentScreen> createState() => _QuranEnrichmentScreenState();
}
class _QuranEnrichmentScreenState extends State<QuranEnrichmentScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _searchController = TextEditingController();
final TextEditingController _pageController = TextEditingController(text: '1');
List<Map<String, dynamic>> _surahs = [];
List<Map<String, dynamic>> _searchResults = [];
List<Map<String, dynamic>> _tafsirItems = [];
List<Map<String, dynamic>> _asbabItems = [];
List<Map<String, dynamic>> _juzItems = [];
List<Map<String, dynamic>> _pageItems = [];
List<Map<String, dynamic>> _themeItems = [];
List<Map<String, dynamic>> _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<String> _expandedWordByWord = {};
final Map<String, List<Map<String, dynamic>>> _wordByWord = {};
final Set<String> _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<void> _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<void> _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<void> _loadTafsirForSelectedSurah() async {
setState(() => _loadingTafsir = true);
final result =
await MuslimApiService.instance.getTafsirBySurah(_selectedSurahId);
if (!mounted) return;
setState(() {
_tafsirItems = result;
_loadingTafsir = false;
});
}
Future<void> _loadAsbabForSelectedSurah() async {
setState(() => _loadingAsbab = true);
final result = await MuslimApiService.instance.getAsbabBySurah(_selectedSurahId);
if (!mounted) return;
setState(() {
_asbabItems = result;
_loadingAsbab = false;
});
}
Future<void> _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<void> _toggleWordByWord(Map<String, dynamic> 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<int> 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<int>(
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<int>(
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,
),
),
);
}
}

View File

@@ -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<QuranMurattalScreen> {
Future<void> _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<QuranMurattalScreen> {
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<QuranMurattalScreen> {
),
),
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<QuranMurattalScreen> {
const SizedBox(height: 8),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
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<QuranMurattalScreen> {
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<QuranMurattalScreen> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final box = Hive.box<AppSettings>(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<QuranMurattalScreen> {
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<QuranMurattalScreen> {
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,

View File

@@ -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<QuranReadingScreen> {
Future<void> _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<QuranReadingScreen> {
),
);
}
} Future<void> _showEndTrackingDialog(TilawahSession session, int endVerseId) async {
}
Future<void> _showEndTrackingDialog(TilawahSession session, int endVerseId) async {
final endSurahId = _surah!['nomor'] ?? 1;
final endSurahName = _surah!['namaLatin'] ?? '';
@@ -367,26 +370,30 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
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<QuranReadingScreen> {
],
),
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<QuranReadingScreen> {
style: TextStyle(
fontFamily: 'Amiri',
fontSize: 26,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 4),
@@ -802,6 +815,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
style: const TextStyle(
fontFamily: 'Amiri',
fontSize: 26,
fontWeight: FontWeight.w400,
height: 2.0,
),
),

View File

@@ -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<QuranScreen> {
}
Future<void> _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<QuranScreen> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final isSimpleMode = box.get('default')?.simpleMode ?? false;
final filtered = _searchQuery.isEmpty
? _surahs
: _surahs
@@ -119,7 +118,15 @@ class _QuranScreenState extends ConsumerState<QuranScreen> {
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<QuranScreen> {
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<QuranScreen> {
style: const TextStyle(
fontFamily: 'Amiri',
fontSize: 18,
fontWeight: FontWeight.w400,
),
),
);

View File

@@ -244,6 +244,70 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
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<SettingsScreen> {
);
}
Widget _buildSegmentSettingCard(
bool isDark, {
required String title,
String? subtitle,
required String value,
required Map<String, String> options,
required ValueChanged<String> 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,

View File

@@ -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<Map<String, dynamic>?>(
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,

107
logo-luxury-theme-brief.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
@@ -83,11 +81,13 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
};
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -150,8 +136,8 @@
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
0534C72D1883289C9A7D2A97 /* Pods */,
B1CD7E90020BCFEFA243FA5B /* Pods */,
C5836D92F95B6223EA2AB79E /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -199,11 +185,25 @@
path = Runner;
sourceTree = "<group>";
};
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 = "<group>";
};
C5836D92F95B6223EA2AB79E /* Frameworks */ = {
isa = PBXGroup;
children = (
0696C611C50F37ED234934AD /* Pods_Runner.framework */,
9AEFCC482F4CDCD13DE76DF7 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB