Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a23739d717 | ||
|
|
b62164dde5 | ||
|
|
5cd9a88682 | ||
|
|
e1881acc18 | ||
|
|
f4b914a2ad | ||
|
|
34ef156538 | ||
|
|
cfb551328b | ||
|
|
40366f0708 | ||
|
|
2e40786bcd | ||
|
|
38bcf01ae3 | ||
|
|
00ab05eefc | ||
|
|
f8e293f823 | ||
|
|
c4f0ca785e | ||
|
|
6cc0de8945 |
@@ -15,7 +15,7 @@ migration:
|
|||||||
- platform: root
|
- platform: root
|
||||||
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
||||||
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
||||||
- platform: android
|
- platform: ios
|
||||||
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
||||||
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,17 @@ Migrations live in [`pocketbase/pb_migrations/`](pocketbase/pb_migrations/). The
|
|||||||
| `1740000003` | `overview_favourites` |
|
| `1740000003` | `overview_favourites` |
|
||||||
| `1740000004`–`0006` | `home_filter_widgets` (+ half width, budget fields) |
|
| `1740000004`–`0006` | `home_filter_widgets` (+ half width, budget fields) |
|
||||||
| `1740000007` | `app_prefs` (Home section toggles + Look ahead years) |
|
| `1740000007` | `app_prefs` (Home section toggles + Look ahead years) |
|
||||||
|
| `1740000008` | `home_filter_widgets.label_match` (AND/OR) |
|
||||||
|
| `1740000009` | `usersapp.is_admin` (admin accounts) |
|
||||||
|
|
||||||
Built-in labels stay in the app; only **custom** labels sync. Rows are scoped per user (`user = @request.auth.id`).
|
Built-in labels stay in the app; only **custom** labels sync. Rows are scoped per user (`user = @request.auth.id`).
|
||||||
|
|
||||||
|
### Admin accounts
|
||||||
|
|
||||||
|
Migration `1740000009` adds `is_admin` (bool) on **`usersapp`**. In PocketBase Admin → **Users** → edit a user → enable **is_admin**.
|
||||||
|
|
||||||
|
Admins see an **Admin** badge on Account and can **Create test transaction** (saved locally and synced with bank transactions). Normal users do not get that action. Re-sign-in after flipping the flag so the app picks it up.
|
||||||
|
|
||||||
### Apply migrations (typical Linux host)
|
### Apply migrations (typical Linux host)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ analyzer:
|
|||||||
exclude:
|
exclude:
|
||||||
- build/**
|
- build/**
|
||||||
- android/**
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 60;
|
||||||
|
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 */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
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>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 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 = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
);
|
||||||
|
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 = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
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;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
|
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 = 2620;
|
||||||
|
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;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
|
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 37Z4F445LJ;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.davmar.ingtrakapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
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.ingtrak.myapp.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.ingtrak.myapp.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.ingtrak.myapp.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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 37Z4F445LJ;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.davmar.ingtrakapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
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;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 37Z4F445LJ;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.davmar.ingtrakapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
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 */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<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 = "Release"
|
||||||
|
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>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -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.
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</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="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-101" y="12"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -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>Myapp</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>myapp</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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
// !$*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 */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
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>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 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 = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
);
|
||||||
|
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 = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
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;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
|
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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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.ingtrak.myapp;
|
||||||
|
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.ingtrak.myapp.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.ingtrak.myapp.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.ingtrak.myapp.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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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 = 15.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
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.ingtrak.myapp;
|
||||||
|
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.ingtrak.myapp;
|
||||||
|
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 */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "[email protected]",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -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.
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>Myapp</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>myapp</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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ class BankTransaction {
|
|||||||
required this.amount,
|
required this.amount,
|
||||||
required this.currency,
|
required this.currency,
|
||||||
required this.pending,
|
required this.pending,
|
||||||
|
this.isManual = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String date;
|
final String date;
|
||||||
@@ -96,6 +97,9 @@ class BankTransaction {
|
|||||||
final String currency;
|
final String currency;
|
||||||
final bool pending;
|
final bool pending;
|
||||||
|
|
||||||
|
/// Admin-created test row (editable / deletable); not from bank feed or CSV.
|
||||||
|
final bool isManual;
|
||||||
|
|
||||||
/// Stable local key used for per-occurrence annotations.
|
/// Stable local key used for per-occurrence annotations.
|
||||||
String get id => '$date|$currency|$amount|$description';
|
String get id => '$date|$currency|$amount|$description';
|
||||||
|
|
||||||
@@ -109,6 +113,50 @@ class BankTransaction {
|
|||||||
/// Exact match for copying notes (same description + amount + currency).
|
/// Exact match for copying notes (same description + amount + currency).
|
||||||
String get exactKey => '$descriptionKey|$amount|$currency';
|
String get exactKey => '$descriptionKey|$amount|$currency';
|
||||||
|
|
||||||
|
static String stripManualStamp(String description) =>
|
||||||
|
description.replaceFirst(RegExp(r' ·#\d+$'), '');
|
||||||
|
|
||||||
|
/// True when [other] is the same row for delete/tombstone matching.
|
||||||
|
///
|
||||||
|
/// Uses id first, then date + cents + currency + description (stamp-insensitive),
|
||||||
|
/// so PocketBase float/amount rehydration cannot bypass a local delete.
|
||||||
|
bool matchesForDelete(BankTransaction other) {
|
||||||
|
if (id == other.id) return true;
|
||||||
|
if (date != other.date) return false;
|
||||||
|
if (currency.toUpperCase() != other.currency.toUpperCase()) return false;
|
||||||
|
if ((amount * 100).round() != (other.amount * 100).round()) return false;
|
||||||
|
if (descriptionKey == other.descriptionKey) return true;
|
||||||
|
return stripManualStamp(description).trim().toLowerCase() ==
|
||||||
|
stripManualStamp(other.description).trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
BankTransaction copyWith({
|
||||||
|
String? date,
|
||||||
|
String? description,
|
||||||
|
double? amount,
|
||||||
|
String? currency,
|
||||||
|
bool? pending,
|
||||||
|
bool? isManual,
|
||||||
|
}) {
|
||||||
|
return BankTransaction(
|
||||||
|
date: date ?? this.date,
|
||||||
|
description: description ?? this.description,
|
||||||
|
amount: amount ?? this.amount,
|
||||||
|
currency: currency ?? this.currency,
|
||||||
|
pending: pending ?? this.pending,
|
||||||
|
isManual: isManual ?? this.isManual,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'date': date,
|
||||||
|
'description': description,
|
||||||
|
'amount': amount,
|
||||||
|
'currency': currency,
|
||||||
|
'pending': pending,
|
||||||
|
if (isManual) 'isManual': true,
|
||||||
|
};
|
||||||
|
|
||||||
factory BankTransaction.fromJson(Map<String, dynamic> json) {
|
factory BankTransaction.fromJson(Map<String, dynamic> json) {
|
||||||
final rawAmount = json['amount'];
|
final rawAmount = json['amount'];
|
||||||
final amount = switch (rawAmount) {
|
final amount = switch (rawAmount) {
|
||||||
@@ -122,6 +170,9 @@ class BankTransaction {
|
|||||||
amount: amount,
|
amount: amount,
|
||||||
currency: json['currency'] as String? ?? 'EUR',
|
currency: json['currency'] as String? ?? 'EUR',
|
||||||
pending: json['pending'] as bool? ?? false,
|
pending: json['pending'] as bool? ?? false,
|
||||||
|
isManual: json['isManual'] as bool? ??
|
||||||
|
json['is_manual'] as bool? ??
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class HomeFilterWidget {
|
|||||||
this.budgetFixedAmount = 0,
|
this.budgetFixedAmount = 0,
|
||||||
this.budgetAverageMonths = 3,
|
this.budgetAverageMonths = 3,
|
||||||
this.labelIds = const [],
|
this.labelIds = const [],
|
||||||
|
this.labelMatch = LabelMatchMode.and,
|
||||||
this.flow = TxFlowFilter.all,
|
this.flow = TxFlowFilter.all,
|
||||||
this.amountRange = const TxAmountRange(),
|
this.amountRange = const TxAmountRange(),
|
||||||
this.query = '',
|
this.query = '',
|
||||||
@@ -93,6 +94,8 @@ class HomeFilterWidget {
|
|||||||
this.showAmountIn = true,
|
this.showAmountIn = true,
|
||||||
this.showAmountOut = true,
|
this.showAmountOut = true,
|
||||||
this.showCount = true,
|
this.showCount = true,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deleted = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@@ -115,6 +118,10 @@ class HomeFilterWidget {
|
|||||||
final int budgetAverageMonths;
|
final int budgetAverageMonths;
|
||||||
|
|
||||||
final List<String> labelIds;
|
final List<String> labelIds;
|
||||||
|
|
||||||
|
/// AND vs OR when multiple [labelIds] are selected.
|
||||||
|
final LabelMatchMode labelMatch;
|
||||||
|
|
||||||
final TxFlowFilter flow;
|
final TxFlowFilter flow;
|
||||||
final TxAmountRange amountRange;
|
final TxAmountRange amountRange;
|
||||||
final String query;
|
final String query;
|
||||||
@@ -124,10 +131,17 @@ class HomeFilterWidget {
|
|||||||
final bool showAmountOut;
|
final bool showAmountOut;
|
||||||
final bool showCount;
|
final bool showCount;
|
||||||
|
|
||||||
|
/// Epoch ms; used for last-write-wins sync across devices.
|
||||||
|
final int updatedAt;
|
||||||
|
|
||||||
|
/// Soft-delete so removals propagate via sync instead of being recreated.
|
||||||
|
final bool deleted;
|
||||||
|
|
||||||
IconData get icon => HomeWidgetIcons.iconFor(iconId);
|
IconData get icon => HomeWidgetIcons.iconFor(iconId);
|
||||||
|
|
||||||
TransactionFilterSelection get filter => TransactionFilterSelection(
|
TransactionFilterSelection get filter => TransactionFilterSelection(
|
||||||
labelIds: labelIds.toSet(),
|
labelIds: labelIds.toSet(),
|
||||||
|
labelMatch: labelMatch,
|
||||||
flow: flow,
|
flow: flow,
|
||||||
amountRange: amountRange,
|
amountRange: amountRange,
|
||||||
query: query,
|
query: query,
|
||||||
@@ -147,6 +161,7 @@ class HomeFilterWidget {
|
|||||||
double? budgetFixedAmount,
|
double? budgetFixedAmount,
|
||||||
int? budgetAverageMonths,
|
int? budgetAverageMonths,
|
||||||
List<String>? labelIds,
|
List<String>? labelIds,
|
||||||
|
LabelMatchMode? labelMatch,
|
||||||
TxFlowFilter? flow,
|
TxFlowFilter? flow,
|
||||||
TxAmountRange? amountRange,
|
TxAmountRange? amountRange,
|
||||||
String? query,
|
String? query,
|
||||||
@@ -154,6 +169,9 @@ class HomeFilterWidget {
|
|||||||
bool? showAmountIn,
|
bool? showAmountIn,
|
||||||
bool? showAmountOut,
|
bool? showAmountOut,
|
||||||
bool? showCount,
|
bool? showCount,
|
||||||
|
int? updatedAt,
|
||||||
|
bool? deleted,
|
||||||
|
bool touchUpdatedAt = false,
|
||||||
}) {
|
}) {
|
||||||
return HomeFilterWidget(
|
return HomeFilterWidget(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -168,6 +186,7 @@ class HomeFilterWidget {
|
|||||||
budgetFixedAmount: budgetFixedAmount ?? this.budgetFixedAmount,
|
budgetFixedAmount: budgetFixedAmount ?? this.budgetFixedAmount,
|
||||||
budgetAverageMonths: budgetAverageMonths ?? this.budgetAverageMonths,
|
budgetAverageMonths: budgetAverageMonths ?? this.budgetAverageMonths,
|
||||||
labelIds: labelIds ?? this.labelIds,
|
labelIds: labelIds ?? this.labelIds,
|
||||||
|
labelMatch: labelMatch ?? this.labelMatch,
|
||||||
flow: flow ?? this.flow,
|
flow: flow ?? this.flow,
|
||||||
amountRange: amountRange ?? this.amountRange,
|
amountRange: amountRange ?? this.amountRange,
|
||||||
query: query ?? this.query,
|
query: query ?? this.query,
|
||||||
@@ -175,6 +194,10 @@ class HomeFilterWidget {
|
|||||||
showAmountIn: showAmountIn ?? this.showAmountIn,
|
showAmountIn: showAmountIn ?? this.showAmountIn,
|
||||||
showAmountOut: showAmountOut ?? this.showAmountOut,
|
showAmountOut: showAmountOut ?? this.showAmountOut,
|
||||||
showCount: showCount ?? this.showCount,
|
showCount: showCount ?? this.showCount,
|
||||||
|
updatedAt: touchUpdatedAt
|
||||||
|
? DateTime.now().millisecondsSinceEpoch
|
||||||
|
: (updatedAt ?? this.updatedAt),
|
||||||
|
deleted: deleted ?? this.deleted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +215,7 @@ class HomeFilterWidget {
|
|||||||
display: display,
|
display: display,
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
labelIds: filter.labelIds.toList(growable: false),
|
labelIds: filter.labelIds.toList(growable: false),
|
||||||
|
labelMatch: filter.labelMatch,
|
||||||
flow: filter.flow,
|
flow: filter.flow,
|
||||||
amountRange: filter.amountRange,
|
amountRange: filter.amountRange,
|
||||||
query: filter.query,
|
query: filter.query,
|
||||||
@@ -199,6 +223,7 @@ class HomeFilterWidget {
|
|||||||
showAmountIn: showIn,
|
showAmountIn: showIn,
|
||||||
showAmountOut: showOut,
|
showAmountOut: showOut,
|
||||||
showCount: true,
|
showCount: true,
|
||||||
|
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +240,7 @@ class HomeFilterWidget {
|
|||||||
'budgetFixedAmount': budgetFixedAmount,
|
'budgetFixedAmount': budgetFixedAmount,
|
||||||
'budgetAverageMonths': budgetAverageMonths,
|
'budgetAverageMonths': budgetAverageMonths,
|
||||||
'labelIds': labelIds,
|
'labelIds': labelIds,
|
||||||
|
'labelMatch': labelMatch.name,
|
||||||
'flow': flow.name,
|
'flow': flow.name,
|
||||||
'amountRange': {
|
'amountRange': {
|
||||||
if (amountRange.exact != null) 'exact': amountRange.exact,
|
if (amountRange.exact != null) 'exact': amountRange.exact,
|
||||||
@@ -226,6 +252,8 @@ class HomeFilterWidget {
|
|||||||
'showAmountIn': showAmountIn,
|
'showAmountIn': showAmountIn,
|
||||||
'showAmountOut': showAmountOut,
|
'showAmountOut': showAmountOut,
|
||||||
'showCount': showCount,
|
'showCount': showCount,
|
||||||
|
'updatedAt': updatedAt,
|
||||||
|
'deleted': deleted,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory HomeFilterWidget.fromJson(Map<String, dynamic> json) {
|
factory HomeFilterWidget.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -274,6 +302,10 @@ class HomeFilterWidget {
|
|||||||
for (final id in (json['labelIds'] as List? ?? const []))
|
for (final id in (json['labelIds'] as List? ?? const []))
|
||||||
if (id is String && id.isNotEmpty) id,
|
if (id is String && id.isNotEmpty) id,
|
||||||
],
|
],
|
||||||
|
labelMatch: LabelMatchMode.values.firstWhere(
|
||||||
|
(e) => e.name == json['labelMatch'],
|
||||||
|
orElse: () => LabelMatchMode.and,
|
||||||
|
),
|
||||||
flow: TxFlowFilter.values.firstWhere(
|
flow: TxFlowFilter.values.firstWhere(
|
||||||
(e) => e.name == json['flow'],
|
(e) => e.name == json['flow'],
|
||||||
orElse: () => TxFlowFilter.all,
|
orElse: () => TxFlowFilter.all,
|
||||||
@@ -288,21 +320,31 @@ class HomeFilterWidget {
|
|||||||
showAmountIn: json['showAmountIn'] as bool? ?? true,
|
showAmountIn: json['showAmountIn'] as bool? ?? true,
|
||||||
showAmountOut: json['showAmountOut'] as bool? ?? true,
|
showAmountOut: json['showAmountOut'] as bool? ?? true,
|
||||||
showCount: json['showCount'] as bool? ?? true,
|
showCount: json['showCount'] as bool? ?? true,
|
||||||
|
updatedAt: asInt(json['updatedAt'] ?? json['updated_at'], 0),
|
||||||
|
deleted: json['deleted'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Union home widgets by id (local wins on conflict).
|
/// Union home widgets by id with last-write-wins on [HomeFilterWidget.updatedAt].
|
||||||
List<HomeFilterWidget> mergeHomeFilterWidgets(
|
List<HomeFilterWidget> mergeHomeFilterWidgets(
|
||||||
List<HomeFilterWidget> local,
|
List<HomeFilterWidget> local,
|
||||||
List<HomeFilterWidget> remote,
|
List<HomeFilterWidget> remote,
|
||||||
) {
|
) {
|
||||||
final byId = <String, HomeFilterWidget>{
|
final byId = <String, HomeFilterWidget>{};
|
||||||
for (final w in remote) w.id: w,
|
void put(HomeFilterWidget w) {
|
||||||
};
|
final existing = byId[w.id];
|
||||||
for (final w in local) {
|
if (existing == null || w.updatedAt >= existing.updatedAt) {
|
||||||
byId[w.id] = w;
|
byId[w.id] = w;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final w in remote) {
|
||||||
|
put(w);
|
||||||
|
}
|
||||||
|
for (final w in local) {
|
||||||
|
put(w);
|
||||||
|
}
|
||||||
final merged = byId.values.toList()
|
final merged = byId.values.toList()
|
||||||
..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||||
return merged;
|
return merged;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class OverviewFavourite {
|
|||||||
this.flow = TxFlowFilter.expense,
|
this.flow = TxFlowFilter.expense,
|
||||||
this.barStyle = LabelChartBarStyle.parallel,
|
this.barStyle = LabelChartBarStyle.parallel,
|
||||||
this.invertExpensePerspective = false,
|
this.invertExpensePerspective = false,
|
||||||
|
this.updatedAt = 0,
|
||||||
|
this.deleted = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@@ -34,6 +36,12 @@ class OverviewFavourite {
|
|||||||
final LabelChartBarStyle barStyle;
|
final LabelChartBarStyle barStyle;
|
||||||
final bool invertExpensePerspective;
|
final bool invertExpensePerspective;
|
||||||
|
|
||||||
|
/// Epoch ms; used for last-write-wins sync across devices.
|
||||||
|
final int updatedAt;
|
||||||
|
|
||||||
|
/// Soft-delete so removals propagate via sync instead of being recreated.
|
||||||
|
final bool deleted;
|
||||||
|
|
||||||
OverviewFavourite copyWith({
|
OverviewFavourite copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? name,
|
String? name,
|
||||||
@@ -44,6 +52,9 @@ class OverviewFavourite {
|
|||||||
TxFlowFilter? flow,
|
TxFlowFilter? flow,
|
||||||
LabelChartBarStyle? barStyle,
|
LabelChartBarStyle? barStyle,
|
||||||
bool? invertExpensePerspective,
|
bool? invertExpensePerspective,
|
||||||
|
int? updatedAt,
|
||||||
|
bool? deleted,
|
||||||
|
bool touchUpdatedAt = false,
|
||||||
}) {
|
}) {
|
||||||
return OverviewFavourite(
|
return OverviewFavourite(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -56,6 +67,10 @@ class OverviewFavourite {
|
|||||||
barStyle: barStyle ?? this.barStyle,
|
barStyle: barStyle ?? this.barStyle,
|
||||||
invertExpensePerspective:
|
invertExpensePerspective:
|
||||||
invertExpensePerspective ?? this.invertExpensePerspective,
|
invertExpensePerspective ?? this.invertExpensePerspective,
|
||||||
|
updatedAt: touchUpdatedAt
|
||||||
|
? DateTime.now().millisecondsSinceEpoch
|
||||||
|
: (updatedAt ?? this.updatedAt),
|
||||||
|
deleted: deleted ?? this.deleted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +84,18 @@ class OverviewFavourite {
|
|||||||
'flow': flow.name,
|
'flow': flow.name,
|
||||||
'barStyle': barStyle.name,
|
'barStyle': barStyle.name,
|
||||||
'invertExpensePerspective': invertExpensePerspective,
|
'invertExpensePerspective': invertExpensePerspective,
|
||||||
|
'updatedAt': updatedAt,
|
||||||
|
'deleted': deleted,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory OverviewFavourite.fromJson(Map<String, dynamic> json) {
|
factory OverviewFavourite.fromJson(Map<String, dynamic> json) {
|
||||||
|
final rawUpdated = json['updatedAt'] ?? json['updated_at'];
|
||||||
|
final updatedAt = switch (rawUpdated) {
|
||||||
|
int n => n,
|
||||||
|
num n => n.round(),
|
||||||
|
String s => int.tryParse(s) ?? 0,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
return OverviewFavourite(
|
return OverviewFavourite(
|
||||||
id: json['id'] as String? ?? 'fav_${DateTime.now().millisecondsSinceEpoch}',
|
id: json['id'] as String? ?? 'fav_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
name: (json['name'] as String?)?.trim().isNotEmpty == true
|
name: (json['name'] as String?)?.trim().isNotEmpty == true
|
||||||
@@ -100,6 +124,8 @@ class OverviewFavourite {
|
|||||||
),
|
),
|
||||||
invertExpensePerspective:
|
invertExpensePerspective:
|
||||||
json['invertExpensePerspective'] as bool? ?? false,
|
json['invertExpensePerspective'] as bool? ?? false,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deleted: json['deleted'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,3 +133,25 @@ class OverviewFavourite {
|
|||||||
enum OverviewMetricMode { flow, labels }
|
enum OverviewMetricMode { flow, labels }
|
||||||
|
|
||||||
enum OverviewPeriodMode { monthly, yearly }
|
enum OverviewPeriodMode { monthly, yearly }
|
||||||
|
|
||||||
|
/// Union favourites by id with last-write-wins on [OverviewFavourite.updatedAt].
|
||||||
|
List<OverviewFavourite> mergeOverviewFavourites(
|
||||||
|
List<OverviewFavourite> local,
|
||||||
|
List<OverviewFavourite> remote,
|
||||||
|
) {
|
||||||
|
final byId = <String, OverviewFavourite>{};
|
||||||
|
void put(OverviewFavourite fav) {
|
||||||
|
final existing = byId[fav.id];
|
||||||
|
if (existing == null || fav.updatedAt >= existing.updatedAt) {
|
||||||
|
byId[fav.id] = fav;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final fav in remote) {
|
||||||
|
put(fav);
|
||||||
|
}
|
||||||
|
for (final fav in local) {
|
||||||
|
put(fav);
|
||||||
|
}
|
||||||
|
return byId.values.toList(growable: false);
|
||||||
|
}
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ int labelVisibleDepth(
|
|||||||
/// Filter-only id for txs with no labels (not an assignable category).
|
/// Filter-only id for txs with no labels (not an assignable category).
|
||||||
const String kUncategorizedFilterId = '__uncategorized__';
|
const String kUncategorizedFilterId = '__uncategorized__';
|
||||||
|
|
||||||
|
/// How multiple selected labels combine in a transaction filter.
|
||||||
|
enum LabelMatchMode {
|
||||||
|
/// Tx must include every selected label.
|
||||||
|
and,
|
||||||
|
|
||||||
|
/// Tx must include at least one selected label.
|
||||||
|
or,
|
||||||
|
}
|
||||||
|
|
||||||
/// Chip presentation for [kUncategorizedFilterId] refine filters.
|
/// Chip presentation for [kUncategorizedFilterId] refine filters.
|
||||||
const TxLabel kUncategorizedFilterLabel = TxLabel(
|
const TxLabel kUncategorizedFilterLabel = TxLabel(
|
||||||
id: kUncategorizedFilterId,
|
id: kUncategorizedFilterId,
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ class AuthController extends ChangeNotifier {
|
|||||||
|
|
||||||
RecordModel? get user => _client?.authStore.record;
|
RecordModel? get user => _client?.authStore.record;
|
||||||
|
|
||||||
|
/// True when PocketBase `usersapp.is_admin` is set for this account.
|
||||||
|
bool get isAdmin {
|
||||||
|
final record = user;
|
||||||
|
if (record == null) return false;
|
||||||
|
return record.getBoolValue('is_admin');
|
||||||
|
}
|
||||||
|
|
||||||
String? get email {
|
String? get email {
|
||||||
final value = user?.getStringValue('email');
|
final value = user?.getStringValue('email');
|
||||||
return value == null || value.isEmpty ? null : value;
|
return value == null || value.isEmpty ? null : value;
|
||||||
@@ -80,6 +87,17 @@ class AuthController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refresh the auth record (e.g. after `is_admin` was toggled in Admin UI).
|
||||||
|
Future<void> refreshSession() async {
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
try {
|
||||||
|
await client.collection(kPocketBaseUsersCollection).authRefresh();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('AuthController.refreshSession failed: $error\n$stackTrace');
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
client.authStore.clear();
|
client.authStore.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// [ChangeNotifier] for bank link status, balance, and transactions.
|
/// [ChangeNotifier] for bank link status, balance, and transactions.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../models/bank.dart';
|
import '../models/bank.dart';
|
||||||
@@ -30,9 +32,34 @@ class BankController extends ChangeNotifier {
|
|||||||
_remoteTransactions = cached.transactions;
|
_remoteTransactions = cached.transactions;
|
||||||
}
|
}
|
||||||
_importedTransactions = _storage?.loadImportedTransactions() ?? const [];
|
_importedTransactions = _storage?.loadImportedTransactions() ?? const [];
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions) _upgradeLegacyManual(tx),
|
||||||
|
];
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions) _upgradeLegacyManual(tx),
|
||||||
|
];
|
||||||
|
_pendingRemoteDeletes = _storage?.loadPendingTxDeletes() ?? const [];
|
||||||
|
// Drop anything still queued for remote delete from the local caches.
|
||||||
|
if (_pendingRemoteDeletes.isNotEmpty) {
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
}
|
||||||
_invalidateMerged();
|
_invalidateMerged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Older admin test rows used a ·#stamp suffix before [isManual] existed.
|
||||||
|
static BankTransaction _upgradeLegacyManual(BankTransaction tx) {
|
||||||
|
if (tx.isManual) return tx;
|
||||||
|
if (!RegExp(r' ·#\d+$').hasMatch(tx.description)) return tx;
|
||||||
|
return tx.copyWith(isManual: true);
|
||||||
|
}
|
||||||
|
|
||||||
final IngtrakApi _api;
|
final IngtrakApi _api;
|
||||||
final StorageService? _storage;
|
final StorageService? _storage;
|
||||||
|
|
||||||
@@ -42,10 +69,116 @@ class BankController extends ChangeNotifier {
|
|||||||
/// cannot leave the device behind the cloud (or wipe cloud-only history).
|
/// cannot leave the device behind the cloud (or wipe cloud-only history).
|
||||||
VoidCallback? onAfterSuccessfulLoad;
|
VoidCallback? onAfterSuccessfulLoad;
|
||||||
|
|
||||||
|
/// Fired when local admin txs are removed/replaced and need a remote delete.
|
||||||
|
VoidCallback? onPendingRemoteDeletesChanged;
|
||||||
|
|
||||||
ConnectStatus _status = ConnectStatus.disconnected;
|
ConnectStatus _status = ConnectStatus.disconnected;
|
||||||
BankBalance? _balance;
|
BankBalance? _balance;
|
||||||
List<BankTransaction> _remoteTransactions = const [];
|
List<BankTransaction> _remoteTransactions = const [];
|
||||||
List<BankTransaction> _importedTransactions = const [];
|
List<BankTransaction> _importedTransactions = const [];
|
||||||
|
List<BankTransaction> _pendingRemoteDeletes = const [];
|
||||||
|
|
||||||
|
/// Tombstoned txs waiting to be removed from PocketBase (also persisted).
|
||||||
|
List<BankTransaction> get pendingRemoteDeletes =>
|
||||||
|
List<BankTransaction>.unmodifiable(_pendingRemoteDeletes);
|
||||||
|
|
||||||
|
bool get hasPendingRemoteDeletes => _pendingRemoteDeletes.isNotEmpty;
|
||||||
|
|
||||||
|
bool _isTombstoned(BankTransaction tx) {
|
||||||
|
for (final tomb in _pendingRemoteDeletes) {
|
||||||
|
if (tx.matchesForDelete(tomb)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requeueRemoteDeletes(Iterable<BankTransaction> txs) {
|
||||||
|
for (final tx in txs) {
|
||||||
|
_queueRemoteDelete(tx, notify: false);
|
||||||
|
}
|
||||||
|
unawaited(_persistPendingDeletes());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _queueRemoteDelete(BankTransaction tx, {bool notify = true}) {
|
||||||
|
if (tx.id.isEmpty) return;
|
||||||
|
if (_isTombstoned(tx)) return;
|
||||||
|
_pendingRemoteDeletes = [
|
||||||
|
..._pendingRemoteDeletes,
|
||||||
|
tx.copyWith(isManual: true),
|
||||||
|
];
|
||||||
|
unawaited(_persistPendingDeletes());
|
||||||
|
if (notify) onPendingRemoteDeletesChanged?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge delete tombstones from another device and strip matching local rows.
|
||||||
|
Future<void> applyRemoteDeletes(
|
||||||
|
List<BankTransaction> remote, {
|
||||||
|
bool notify = true,
|
||||||
|
}) async {
|
||||||
|
var tombsChanged = false;
|
||||||
|
for (final tomb in remote) {
|
||||||
|
if (_isTombstoned(tomb)) continue;
|
||||||
|
_pendingRemoteDeletes = [
|
||||||
|
..._pendingRemoteDeletes,
|
||||||
|
tomb.copyWith(isManual: true),
|
||||||
|
];
|
||||||
|
tombsChanged = true;
|
||||||
|
}
|
||||||
|
if (_pendingRemoteDeletes.isEmpty) return;
|
||||||
|
|
||||||
|
final beforeRemote = _remoteTransactions.length;
|
||||||
|
final beforeImported = _importedTransactions.length;
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
final stripped = beforeRemote != _remoteTransactions.length ||
|
||||||
|
beforeImported != _importedTransactions.length;
|
||||||
|
|
||||||
|
if (tombsChanged || stripped) {
|
||||||
|
_invalidateMerged();
|
||||||
|
if (tombsChanged) await _persistPendingDeletes();
|
||||||
|
if (stripped) {
|
||||||
|
await _persistCache();
|
||||||
|
await _persistImported();
|
||||||
|
}
|
||||||
|
if (notify) notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistPendingDeletes() async {
|
||||||
|
final storage = _storage;
|
||||||
|
if (storage == null) return;
|
||||||
|
await storage.savePendingTxDeletes(_pendingRemoteDeletes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep [isManual] when a synced/API row replaces a matching imported test tx.
|
||||||
|
void _reapplyManualFlags(Set<String> manualIds) {
|
||||||
|
if (manualIds.isEmpty) return;
|
||||||
|
BankTransaction stamp(BankTransaction tx) =>
|
||||||
|
manualIds.contains(tx.id) && !tx.isManual
|
||||||
|
? tx.copyWith(isManual: true)
|
||||||
|
: tx;
|
||||||
|
_remoteTransactions = [for (final tx in _remoteTransactions) stamp(tx)];
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions) stamp(tx),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> _collectManualIds([
|
||||||
|
Iterable<BankTransaction> extra = const [],
|
||||||
|
]) =>
|
||||||
|
{
|
||||||
|
for (final tx in [
|
||||||
|
..._remoteTransactions,
|
||||||
|
..._importedTransactions,
|
||||||
|
...extra,
|
||||||
|
])
|
||||||
|
if (tx.isManual) tx.id,
|
||||||
|
};
|
||||||
List<BankTransaction>? _mergedTransactions;
|
List<BankTransaction>? _mergedTransactions;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -161,6 +294,117 @@ class BankController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert a locally created transaction (admin testing). Persisted like CSV
|
||||||
|
/// imports and included in PocketBase `transactions` sync.
|
||||||
|
Future<BankTransaction> addManualTransaction({
|
||||||
|
required DateTime date,
|
||||||
|
required String description,
|
||||||
|
required double amount,
|
||||||
|
String currency = 'EUR',
|
||||||
|
bool pending = false,
|
||||||
|
}) async {
|
||||||
|
final desc =
|
||||||
|
description.trim().isEmpty ? 'Test transaction' : description.trim();
|
||||||
|
// Include a short unique suffix so identical drafts do not collide on id.
|
||||||
|
final stamp = DateTime.now().millisecondsSinceEpoch % 100000;
|
||||||
|
final tx = BankTransaction(
|
||||||
|
date:
|
||||||
|
'${date.year.toString().padLeft(4, '0')}-'
|
||||||
|
'${date.month.toString().padLeft(2, '0')}-'
|
||||||
|
'${date.day.toString().padLeft(2, '0')}',
|
||||||
|
description: '$desc ·#$stamp',
|
||||||
|
amount: amount,
|
||||||
|
currency: currency.trim().isEmpty ? 'EUR' : currency.trim().toUpperCase(),
|
||||||
|
pending: pending,
|
||||||
|
isManual: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
_importedTransactions = mergeTransactions(_importedTransactions, [tx]);
|
||||||
|
_importedTransactions = filterNewTransactions(
|
||||||
|
_remoteTransactions,
|
||||||
|
_importedTransactions,
|
||||||
|
);
|
||||||
|
_invalidateMerged();
|
||||||
|
await _persistImported();
|
||||||
|
notifyListeners();
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace an admin test transaction. Returns the updated row (id may change).
|
||||||
|
Future<BankTransaction> updateManualTransaction({
|
||||||
|
required BankTransaction previous,
|
||||||
|
required DateTime date,
|
||||||
|
required String description,
|
||||||
|
required double amount,
|
||||||
|
String currency = 'EUR',
|
||||||
|
bool pending = false,
|
||||||
|
}) async {
|
||||||
|
if (!previous.isManual) {
|
||||||
|
throw StateError('Only admin test transactions can be edited');
|
||||||
|
}
|
||||||
|
final desc =
|
||||||
|
description.trim().isEmpty ? 'Test transaction' : description.trim();
|
||||||
|
final stamped = () {
|
||||||
|
if (RegExp(r' ·#\d+$').hasMatch(desc)) return desc;
|
||||||
|
final prevStamp = RegExp(r' ·#(\d+)$').firstMatch(previous.description);
|
||||||
|
final prevBase =
|
||||||
|
previous.description.replaceFirst(RegExp(r' ·#\d+$'), '');
|
||||||
|
if (prevStamp != null && prevBase == desc) {
|
||||||
|
return '$desc ·#${prevStamp.group(1)}';
|
||||||
|
}
|
||||||
|
return '$desc ·#${DateTime.now().millisecondsSinceEpoch % 100000}';
|
||||||
|
}();
|
||||||
|
final updated = BankTransaction(
|
||||||
|
date:
|
||||||
|
'${date.year.toString().padLeft(4, '0')}-'
|
||||||
|
'${date.month.toString().padLeft(2, '0')}-'
|
||||||
|
'${date.day.toString().padLeft(2, '0')}',
|
||||||
|
description: stamped,
|
||||||
|
amount: amount,
|
||||||
|
currency: currency.trim().isEmpty ? 'EUR' : currency.trim().toUpperCase(),
|
||||||
|
pending: pending,
|
||||||
|
isManual: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions)
|
||||||
|
if (tx.id != previous.id) tx,
|
||||||
|
];
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions)
|
||||||
|
if (tx.id != previous.id) tx,
|
||||||
|
];
|
||||||
|
if (previous.id != updated.id) {
|
||||||
|
_queueRemoteDelete(previous);
|
||||||
|
}
|
||||||
|
_importedTransactions = mergeTransactions(_importedTransactions, [updated]);
|
||||||
|
_invalidateMerged();
|
||||||
|
await _persistImported();
|
||||||
|
await _persistCache();
|
||||||
|
notifyListeners();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an admin test transaction from local caches.
|
||||||
|
Future<void> deleteManualTransaction(BankTransaction tx) async {
|
||||||
|
if (!tx.isManual) {
|
||||||
|
throw StateError('Only admin test transactions can be deleted');
|
||||||
|
}
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final row in _importedTransactions)
|
||||||
|
if (!row.matchesForDelete(tx)) row,
|
||||||
|
];
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final row in _remoteTransactions)
|
||||||
|
if (!row.matchesForDelete(tx)) row,
|
||||||
|
];
|
||||||
|
_queueRemoteDelete(tx);
|
||||||
|
_invalidateMerged();
|
||||||
|
await _persistImported();
|
||||||
|
await _persistCache();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Union cloud txs into the local bank cache without dropping either side.
|
/// Union cloud txs into the local bank cache without dropping either side.
|
||||||
///
|
///
|
||||||
/// Rows land in the API cache; CSV imports that match are deduped away.
|
/// Rows land in the API cache; CSV imports that match are deduped away.
|
||||||
@@ -168,13 +412,39 @@ class BankController extends ChangeNotifier {
|
|||||||
List<BankTransaction> fromCloud, {
|
List<BankTransaction> fromCloud, {
|
||||||
bool notify = true,
|
bool notify = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (fromCloud.isEmpty) return;
|
if (fromCloud.isEmpty && _pendingRemoteDeletes.isEmpty) return;
|
||||||
|
|
||||||
_remoteTransactions = mergeTransactions(_remoteTransactions, fromCloud);
|
// Never resurrect rows the user already deleted locally.
|
||||||
|
final incoming = [
|
||||||
|
for (final tx in fromCloud)
|
||||||
|
if (!_isTombstoned(tx)) _upgradeLegacyManual(tx),
|
||||||
|
];
|
||||||
|
if (incoming.isEmpty && _pendingRemoteDeletes.isEmpty) return;
|
||||||
|
|
||||||
|
final manuals = _collectManualIds(incoming);
|
||||||
|
if (incoming.isNotEmpty) {
|
||||||
|
_remoteTransactions = dropSettledPendingDuplicates(
|
||||||
|
mergeTransactions(_remoteTransactions, incoming),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Strip any local copies that match a pending delete (defensive).
|
||||||
|
if (_pendingRemoteDeletes.isNotEmpty) {
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
}
|
||||||
_importedTransactions = filterNewTransactions(
|
_importedTransactions = filterNewTransactions(
|
||||||
_remoteTransactions,
|
_remoteTransactions,
|
||||||
_importedTransactions,
|
_importedTransactions,
|
||||||
);
|
);
|
||||||
|
if (_pendingRemoteDeletes.isNotEmpty) {
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
_reapplyManualFlags(manuals);
|
||||||
_invalidateMerged();
|
_invalidateMerged();
|
||||||
|
|
||||||
if (!_status.linked && _remoteTransactions.isNotEmpty) {
|
if (!_status.linked && _remoteTransactions.isNotEmpty) {
|
||||||
@@ -254,7 +524,10 @@ class BankController extends ChangeNotifier {
|
|||||||
final apiTxs = results[1] as List<BankTransaction>;
|
final apiTxs = results[1] as List<BankTransaction>;
|
||||||
// Bank API is ~89 days. Prefer API rows for overlaps, but keep any
|
// Bank API is ~89 days. Prefer API rows for overlaps, but keep any
|
||||||
// cloud/CSV history that the API window does not include.
|
// cloud/CSV history that the API window does not include.
|
||||||
_remoteTransactions = mergeTransactions(apiTxs, _remoteTransactions);
|
// Pending rows are not kept from cache — they must still be on the feed.
|
||||||
|
final manuals = _collectManualIds();
|
||||||
|
_remoteTransactions =
|
||||||
|
mergeApiTransactionsWithCache(apiTxs, _remoteTransactions);
|
||||||
if (!_status.linked) {
|
if (!_status.linked) {
|
||||||
_status = ConnectStatus(
|
_status = ConnectStatus(
|
||||||
status: 'linked',
|
status: 'linked',
|
||||||
@@ -269,6 +542,17 @@ class BankController extends ChangeNotifier {
|
|||||||
_remoteTransactions,
|
_remoteTransactions,
|
||||||
_importedTransactions,
|
_importedTransactions,
|
||||||
);
|
);
|
||||||
|
if (_pendingRemoteDeletes.isNotEmpty) {
|
||||||
|
_remoteTransactions = [
|
||||||
|
for (final tx in _remoteTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
_importedTransactions = [
|
||||||
|
for (final tx in _importedTransactions)
|
||||||
|
if (!_isTombstoned(tx)) tx,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
_reapplyManualFlags(manuals);
|
||||||
_invalidateMerged();
|
_invalidateMerged();
|
||||||
await _persistImported();
|
await _persistImported();
|
||||||
_error = null;
|
_error = null;
|
||||||
|
|||||||
@@ -42,26 +42,40 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
List<OverviewFavourite> get overviewFavourites =>
|
List<OverviewFavourite> get overviewFavourites =>
|
||||||
|
List.unmodifiable([
|
||||||
|
for (final fav in _overviewFavourites)
|
||||||
|
if (!fav.deleted) fav,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Full list including soft-deleted rows (for PocketBase push).
|
||||||
|
List<OverviewFavourite> get overviewFavouritesForSync =>
|
||||||
List.unmodifiable(_overviewFavourites);
|
List.unmodifiable(_overviewFavourites);
|
||||||
|
|
||||||
List<OverviewFavourite> get displayedOverviewFavourites => [
|
List<OverviewFavourite> get displayedOverviewFavourites => [
|
||||||
for (final fav in _overviewFavourites)
|
for (final fav in _overviewFavourites)
|
||||||
if (fav.display) fav,
|
if (!fav.deleted && fav.display) fav,
|
||||||
];
|
];
|
||||||
|
|
||||||
bool get hasDisplayedOverviewFavourites =>
|
bool get hasDisplayedOverviewFavourites =>
|
||||||
_overviewFavourites.any((f) => f.display);
|
_overviewFavourites.any((f) => !f.deleted && f.display);
|
||||||
|
|
||||||
List<HomeFilterWidget> get homeFilterWidgets =>
|
List<HomeFilterWidget> get homeFilterWidgets =>
|
||||||
|
List.unmodifiable([
|
||||||
|
for (final w in _homeFilterWidgets)
|
||||||
|
if (!w.deleted) w,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Full list including soft-deleted rows (for PocketBase push).
|
||||||
|
List<HomeFilterWidget> get homeFilterWidgetsForSync =>
|
||||||
List.unmodifiable(_homeFilterWidgets);
|
List.unmodifiable(_homeFilterWidgets);
|
||||||
|
|
||||||
List<HomeFilterWidget> get displayedHomeFilterWidgets => [
|
List<HomeFilterWidget> get displayedHomeFilterWidgets => [
|
||||||
for (final w in _homeFilterWidgets)
|
for (final w in _homeFilterWidgets)
|
||||||
if (w.display) w,
|
if (!w.deleted && w.display) w,
|
||||||
];
|
];
|
||||||
|
|
||||||
bool get hasDisplayedHomeFilterWidgets =>
|
bool get hasDisplayedHomeFilterWidgets =>
|
||||||
_homeFilterWidgets.any((w) => w.display);
|
_homeFilterWidgets.any((w) => !w.deleted && w.display);
|
||||||
|
|
||||||
HomeBuiltInSections get homeBuiltInSections => _homeBuiltInSections;
|
HomeBuiltInSections get homeBuiltInSections => _homeBuiltInSections;
|
||||||
|
|
||||||
@@ -118,7 +132,10 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addOverviewFavourite(OverviewFavourite favourite) async {
|
Future<void> addOverviewFavourite(OverviewFavourite favourite) async {
|
||||||
_overviewFavourites = [..._overviewFavourites, favourite];
|
final stamped = favourite.updatedAt == 0
|
||||||
|
? favourite.copyWith(touchUpdatedAt: true)
|
||||||
|
: favourite;
|
||||||
|
_overviewFavourites = [..._overviewFavourites, stamped];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistFavourites();
|
await _persistFavourites();
|
||||||
}
|
}
|
||||||
@@ -127,18 +144,19 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
final index = _overviewFavourites.indexWhere((f) => f.id == favourite.id);
|
final index = _overviewFavourites.indexWhere((f) => f.id == favourite.id);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
final next = [..._overviewFavourites];
|
final next = [..._overviewFavourites];
|
||||||
next[index] = favourite;
|
next[index] = favourite.copyWith(touchUpdatedAt: true);
|
||||||
_overviewFavourites = next;
|
_overviewFavourites = next;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistFavourites();
|
await _persistFavourites();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeOverviewFavourite(String id) async {
|
Future<void> removeOverviewFavourite(String id) async {
|
||||||
final next = [
|
final index = _overviewFavourites.indexWhere((f) => f.id == id);
|
||||||
for (final fav in _overviewFavourites)
|
if (index < 0) return;
|
||||||
if (fav.id != id) fav,
|
final current = _overviewFavourites[index];
|
||||||
];
|
if (current.deleted) return;
|
||||||
if (next.length == _overviewFavourites.length) return;
|
final next = [..._overviewFavourites];
|
||||||
|
next[index] = current.copyWith(deleted: true, touchUpdatedAt: true);
|
||||||
_overviewFavourites = next;
|
_overviewFavourites = next;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistFavourites();
|
await _persistFavourites();
|
||||||
@@ -148,7 +166,7 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
final index = _overviewFavourites.indexWhere((f) => f.id == id);
|
final index = _overviewFavourites.indexWhere((f) => f.id == id);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
final current = _overviewFavourites[index];
|
final current = _overviewFavourites[index];
|
||||||
if (current.display == display) return;
|
if (current.deleted || current.display == display) return;
|
||||||
await updateOverviewFavourite(current.copyWith(display: display));
|
await updateOverviewFavourite(current.copyWith(display: display));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,16 +182,20 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addHomeFilterWidget(HomeFilterWidget widget) async {
|
Future<void> addHomeFilterWidget(HomeFilterWidget widget) async {
|
||||||
final order = _homeFilterWidgets.isEmpty
|
final order = _homeFilterWidgets.where((w) => !w.deleted).isEmpty
|
||||||
? 0
|
? 0
|
||||||
: _homeFilterWidgets
|
: _homeFilterWidgets
|
||||||
|
.where((w) => !w.deleted)
|
||||||
.map((w) => w.sortOrder)
|
.map((w) => w.sortOrder)
|
||||||
.reduce((a, b) => a > b ? a : b) +
|
.reduce((a, b) => a > b ? a : b) +
|
||||||
1;
|
1;
|
||||||
_homeFilterWidgets = [
|
final stamped = (widget.updatedAt == 0
|
||||||
..._homeFilterWidgets,
|
? widget.copyWith(touchUpdatedAt: true)
|
||||||
widget.copyWith(sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder),
|
: widget)
|
||||||
];
|
.copyWith(
|
||||||
|
sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder,
|
||||||
|
);
|
||||||
|
_homeFilterWidgets = [..._homeFilterWidgets, stamped];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistHomeWidgets();
|
await _persistHomeWidgets();
|
||||||
}
|
}
|
||||||
@@ -182,7 +204,7 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
final index = _homeFilterWidgets.indexWhere((w) => w.id == widget.id);
|
final index = _homeFilterWidgets.indexWhere((w) => w.id == widget.id);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
final next = [..._homeFilterWidgets];
|
final next = [..._homeFilterWidgets];
|
||||||
next[index] = widget;
|
next[index] = widget.copyWith(touchUpdatedAt: true);
|
||||||
next.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
next.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||||
_homeFilterWidgets = next;
|
_homeFilterWidgets = next;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -190,11 +212,12 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeHomeFilterWidget(String id) async {
|
Future<void> removeHomeFilterWidget(String id) async {
|
||||||
final next = [
|
final index = _homeFilterWidgets.indexWhere((w) => w.id == id);
|
||||||
for (final w in _homeFilterWidgets)
|
if (index < 0) return;
|
||||||
if (w.id != id) w,
|
final current = _homeFilterWidgets[index];
|
||||||
];
|
if (current.deleted) return;
|
||||||
if (next.length == _homeFilterWidgets.length) return;
|
final next = [..._homeFilterWidgets];
|
||||||
|
next[index] = current.copyWith(deleted: true, touchUpdatedAt: true);
|
||||||
_homeFilterWidgets = next;
|
_homeFilterWidgets = next;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistHomeWidgets();
|
await _persistHomeWidgets();
|
||||||
@@ -204,19 +227,32 @@ class PreferencesController extends ChangeNotifier {
|
|||||||
final index = _homeFilterWidgets.indexWhere((w) => w.id == id);
|
final index = _homeFilterWidgets.indexWhere((w) => w.id == id);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
final current = _homeFilterWidgets[index];
|
final current = _homeFilterWidgets[index];
|
||||||
if (current.display == display) return;
|
if (current.deleted || current.display == display) return;
|
||||||
await updateHomeFilterWidget(current.copyWith(display: display));
|
await updateHomeFilterWidget(current.copyWith(display: display));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reorderHomeFilterWidgets(int oldIndex, int newIndex) async {
|
Future<void> reorderHomeFilterWidgets(int oldIndex, int newIndex) async {
|
||||||
if (oldIndex == newIndex) return;
|
if (oldIndex == newIndex) return;
|
||||||
final next = [..._homeFilterWidgets];
|
final visible = [
|
||||||
final item = next.removeAt(oldIndex);
|
for (final w in _homeFilterWidgets)
|
||||||
final target = newIndex.clamp(0, next.length);
|
if (!w.deleted) w,
|
||||||
next.insert(target, item);
|
|
||||||
_homeFilterWidgets = [
|
|
||||||
for (var i = 0; i < next.length; i++) next[i].copyWith(sortOrder: i),
|
|
||||||
];
|
];
|
||||||
|
if (oldIndex < 0 || oldIndex >= visible.length) return;
|
||||||
|
final nextVisible = [...visible];
|
||||||
|
final item = nextVisible.removeAt(oldIndex);
|
||||||
|
final target = newIndex.clamp(0, nextVisible.length);
|
||||||
|
nextVisible.insert(target, item);
|
||||||
|
final orderById = <String, int>{
|
||||||
|
for (var i = 0; i < nextVisible.length; i++) nextVisible[i].id: i,
|
||||||
|
};
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
_homeFilterWidgets = [
|
||||||
|
for (final w in _homeFilterWidgets)
|
||||||
|
if (w.deleted)
|
||||||
|
w
|
||||||
|
else
|
||||||
|
w.copyWith(sortOrder: orderById[w.id] ?? w.sortOrder, updatedAt: now),
|
||||||
|
]..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _persistHomeWidgets();
|
await _persistHomeWidgets();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../models/app_cloud_prefs.dart';
|
import '../models/app_cloud_prefs.dart';
|
||||||
|
import '../models/bank.dart';
|
||||||
import '../models/home_filter_widget.dart';
|
import '../models/home_filter_widget.dart';
|
||||||
import '../models/overview_favourite.dart';
|
import '../models/overview_favourite.dart';
|
||||||
import '../services/pb_sync_service.dart';
|
import '../services/pb_sync_service.dart';
|
||||||
@@ -46,6 +47,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
_prefs.onAppPrefsPersisted = _scheduleAppPrefsPush;
|
_prefs.onAppPrefsPersisted = _scheduleAppPrefsPush;
|
||||||
_auth.addListener(_onAuthChanged);
|
_auth.addListener(_onAuthChanged);
|
||||||
_bank.onAfterSuccessfulLoad = _onBankDataLoaded;
|
_bank.onAfterSuccessfulLoad = _onBankDataLoaded;
|
||||||
|
_bank.onPendingRemoteDeletesChanged = _scheduleBankTxPush;
|
||||||
if (_auth.isLoggedIn) {
|
if (_auth.isLoggedIn) {
|
||||||
unawaited(syncNow());
|
unawaited(syncNow());
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
bool _pushFavouritesPending = false;
|
bool _pushFavouritesPending = false;
|
||||||
bool _pushHomeWidgetsPending = false;
|
bool _pushHomeWidgetsPending = false;
|
||||||
bool _pushAppPrefsPending = false;
|
bool _pushAppPrefsPending = false;
|
||||||
|
bool _pushBankPending = false;
|
||||||
Timer? _pushDebounce;
|
Timer? _pushDebounce;
|
||||||
Timer? _progressHold;
|
Timer? _progressHold;
|
||||||
Timer? _uiNotifyThrottle;
|
Timer? _uiNotifyThrottle;
|
||||||
@@ -198,6 +201,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
_pushFavouritesPending = false;
|
_pushFavouritesPending = false;
|
||||||
_pushHomeWidgetsPending = false;
|
_pushHomeWidgetsPending = false;
|
||||||
_pushAppPrefsPending = false;
|
_pushAppPrefsPending = false;
|
||||||
|
_pushBankPending = false;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
_statusDetail = '';
|
_statusDetail = '';
|
||||||
_showProgressRing = false;
|
_showProgressRing = false;
|
||||||
@@ -265,6 +269,47 @@ class SyncController extends ChangeNotifier {
|
|||||||
_armPushDebounce();
|
_armPushDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleBankTxPush() {
|
||||||
|
_pushBankPending = true;
|
||||||
|
_armPushDebounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull shared delete tombstones, then remove matching local bank rows.
|
||||||
|
Future<void> _pullAndApplyTxDeletes(PbSyncService service) async {
|
||||||
|
try {
|
||||||
|
final remote = await service.pullTxDeletes(onStatus: _setStatus);
|
||||||
|
await _bank.applyRemoteDeletes(remote, notify: false);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('SyncController: tx_deletes pull skipped ($error)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes tombstoned bank txs from the transactions collection.
|
||||||
|
///
|
||||||
|
/// Tombstones themselves stay local + synced via `tx_deletes` so other
|
||||||
|
/// devices learn about the delete and do not push the row back.
|
||||||
|
Future<void> _flushPendingTxDeletes(PbSyncService service) async {
|
||||||
|
final pending = List<BankTransaction>.of(_bank.pendingRemoteDeletes);
|
||||||
|
if (pending.isEmpty) return;
|
||||||
|
_setStatus('Removing deleted bank txs…');
|
||||||
|
try {
|
||||||
|
await service.deleteTransactionsMatching(pending);
|
||||||
|
// Keep tombstones — they must remain until every device has seen them.
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('SyncController: tx deletes failed ($error)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pushTxDeletes(PbSyncService service) async {
|
||||||
|
final tombs = List<BankTransaction>.of(_bank.pendingRemoteDeletes);
|
||||||
|
if (tombs.isEmpty) return;
|
||||||
|
try {
|
||||||
|
await service.pushTxDeletes(tombs, onStatus: _setStatus);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('SyncController: tx_deletes push skipped ($error)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _armPushDebounce() {
|
void _armPushDebounce() {
|
||||||
if (!_auth.isLoggedIn) return;
|
if (!_auth.isLoggedIn) return;
|
||||||
if (_syncing) {
|
if (_syncing) {
|
||||||
@@ -297,6 +342,9 @@ class SyncController extends ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final service = PbSyncService(_auth.client);
|
final service = PbSyncService(_auth.client);
|
||||||
|
// Learn deletes from other devices before merging bank txs / pushing.
|
||||||
|
await _pullAndApplyTxDeletes(service);
|
||||||
|
await _flushPendingTxDeletes(service);
|
||||||
final remoteMeta = await service.pull(
|
final remoteMeta = await service.pull(
|
||||||
onProgress: (p) {
|
onProgress: (p) {
|
||||||
_setProgress(p * 0.35);
|
_setProgress(p * 0.35);
|
||||||
@@ -324,7 +372,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final mergedFavourites = mergeOverviewFavourites(
|
final mergedFavourites = mergeOverviewFavourites(
|
||||||
_prefs.overviewFavourites,
|
_prefs.overviewFavouritesForSync,
|
||||||
remoteFavourites,
|
remoteFavourites,
|
||||||
);
|
);
|
||||||
List<HomeFilterWidget> remoteHomeWidgets = const [];
|
List<HomeFilterWidget> remoteHomeWidgets = const [];
|
||||||
@@ -338,7 +386,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final mergedHomeWidgets = mergeHomeFilterWidgets(
|
final mergedHomeWidgets = mergeHomeFilterWidgets(
|
||||||
_prefs.homeFilterWidgets,
|
_prefs.homeFilterWidgetsForSync,
|
||||||
remoteHomeWidgets,
|
remoteHomeWidgets,
|
||||||
);
|
);
|
||||||
AppCloudPrefs? remoteAppPrefs;
|
AppCloudPrefs? remoteAppPrefs;
|
||||||
@@ -408,8 +456,14 @@ class SyncController extends ChangeNotifier {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('SyncController: app_prefs push skipped ($error)');
|
debugPrint('SyncController: app_prefs push skipped ($error)');
|
||||||
}
|
}
|
||||||
|
// Retry deletes in case any were queued during the pull/merge window.
|
||||||
|
await _flushPendingTxDeletes(service);
|
||||||
|
await _pushTxDeletes(service);
|
||||||
|
_pushBankPending = false;
|
||||||
|
final tombs = List<BankTransaction>.of(_bank.pendingRemoteDeletes);
|
||||||
final bankSummary = await service.pushTransactions(
|
final bankSummary = await service.pushTransactions(
|
||||||
_bank.transactions,
|
_bank.transactions,
|
||||||
|
deleteTombs: tombs,
|
||||||
progressStart: 0.82,
|
progressStart: 0.82,
|
||||||
progressEnd: 1.0,
|
progressEnd: 1.0,
|
||||||
onProgress: _setProgress,
|
onProgress: _setProgress,
|
||||||
@@ -454,11 +508,17 @@ class SyncController extends ChangeNotifier {
|
|||||||
final pushFavourites = _pushFavouritesPending;
|
final pushFavourites = _pushFavouritesPending;
|
||||||
final pushHomeWidgets = _pushHomeWidgetsPending;
|
final pushHomeWidgets = _pushHomeWidgetsPending;
|
||||||
final pushAppPrefs = _pushAppPrefsPending;
|
final pushAppPrefs = _pushAppPrefsPending;
|
||||||
|
final pushBank = _pushBankPending || _bank.pendingRemoteDeletes.isNotEmpty;
|
||||||
_pushMetaPending = false;
|
_pushMetaPending = false;
|
||||||
_pushFavouritesPending = false;
|
_pushFavouritesPending = false;
|
||||||
_pushHomeWidgetsPending = false;
|
_pushHomeWidgetsPending = false;
|
||||||
_pushAppPrefsPending = false;
|
_pushAppPrefsPending = false;
|
||||||
if (!pushMeta && !pushFavourites && !pushHomeWidgets && !pushAppPrefs) {
|
_pushBankPending = false;
|
||||||
|
if (!pushMeta &&
|
||||||
|
!pushFavourites &&
|
||||||
|
!pushHomeWidgets &&
|
||||||
|
!pushAppPrefs &&
|
||||||
|
!pushBank) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,13 +531,15 @@ class SyncController extends ChangeNotifier {
|
|||||||
pushFavourites,
|
pushFavourites,
|
||||||
pushHomeWidgets,
|
pushHomeWidgets,
|
||||||
pushAppPrefs,
|
pushAppPrefs,
|
||||||
|
pushBank,
|
||||||
)) {
|
)) {
|
||||||
(false, false, false, true) => 'App prefs',
|
(false, false, false, false, true) => 'Bank txs',
|
||||||
(false, false, true, false) => 'Home widgets',
|
(false, false, false, true, false) => 'App prefs',
|
||||||
(false, false, true, true) => 'Home layout',
|
(false, false, true, false, false) => 'Home widgets',
|
||||||
(false, true, false, false) => 'Favourites',
|
(false, false, true, true, _) => 'Home layout',
|
||||||
(false, true, true, _) => 'Home layout',
|
(false, true, false, false, false) => 'Favourites',
|
||||||
(true, false, false, false) => 'Annotations',
|
(false, true, true, _, _) => 'Home layout',
|
||||||
|
(true, false, false, false, false) => 'Annotations',
|
||||||
_ => 'Changes',
|
_ => 'Changes',
|
||||||
};
|
};
|
||||||
_statusDetail = 'Pushing $label…';
|
_statusDetail = 'Pushing $label…';
|
||||||
@@ -491,7 +553,8 @@ class SyncController extends ChangeNotifier {
|
|||||||
final total = (pushMeta ? 1 : 0) +
|
final total = (pushMeta ? 1 : 0) +
|
||||||
(pushFavourites ? 1 : 0) +
|
(pushFavourites ? 1 : 0) +
|
||||||
(pushHomeWidgets ? 1 : 0) +
|
(pushHomeWidgets ? 1 : 0) +
|
||||||
(pushAppPrefs ? 1 : 0);
|
(pushAppPrefs ? 1 : 0) +
|
||||||
|
(pushBank ? 1 : 0);
|
||||||
void bump() {
|
void bump() {
|
||||||
done++;
|
done++;
|
||||||
_setProgress(done / total);
|
_setProgress(done / total);
|
||||||
@@ -513,7 +576,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
_setStatus('Pushing favourites…');
|
_setStatus('Pushing favourites…');
|
||||||
try {
|
try {
|
||||||
await service.pushOverviewFavourites(
|
await service.pushOverviewFavourites(
|
||||||
_prefs.overviewFavourites,
|
_prefs.overviewFavouritesForSync,
|
||||||
onStatus: _setStatus,
|
onStatus: _setStatus,
|
||||||
);
|
);
|
||||||
parts.add('Favourites updated');
|
parts.add('Favourites updated');
|
||||||
@@ -521,6 +584,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'SyncController: overview_favourites push skipped ($error)',
|
'SyncController: overview_favourites push skipped ($error)',
|
||||||
);
|
);
|
||||||
|
_pushFavouritesPending = true;
|
||||||
}
|
}
|
||||||
bump();
|
bump();
|
||||||
}
|
}
|
||||||
@@ -528,7 +592,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
_setStatus('Pushing home widgets…');
|
_setStatus('Pushing home widgets…');
|
||||||
try {
|
try {
|
||||||
await service.pushHomeFilterWidgets(
|
await service.pushHomeFilterWidgets(
|
||||||
_prefs.homeFilterWidgets,
|
_prefs.homeFilterWidgetsForSync,
|
||||||
onStatus: _setStatus,
|
onStatus: _setStatus,
|
||||||
);
|
);
|
||||||
parts.add('Home widgets updated');
|
parts.add('Home widgets updated');
|
||||||
@@ -536,6 +600,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'SyncController: home_filter_widgets push skipped ($error)',
|
'SyncController: home_filter_widgets push skipped ($error)',
|
||||||
);
|
);
|
||||||
|
_pushHomeWidgetsPending = true;
|
||||||
}
|
}
|
||||||
bump();
|
bump();
|
||||||
}
|
}
|
||||||
@@ -555,6 +620,29 @@ class SyncController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
bump();
|
bump();
|
||||||
}
|
}
|
||||||
|
if (pushBank) {
|
||||||
|
_setStatus('Updating bank txs…');
|
||||||
|
// Other devices may have deleted test txs — learn that before push.
|
||||||
|
await _pullAndApplyTxDeletes(service);
|
||||||
|
await _flushPendingTxDeletes(service);
|
||||||
|
await _pushTxDeletes(service);
|
||||||
|
try {
|
||||||
|
final tombs = List<BankTransaction>.of(_bank.pendingRemoteDeletes);
|
||||||
|
final summary = await service.pushTransactions(
|
||||||
|
_bank.transactions,
|
||||||
|
deleteTombs: tombs,
|
||||||
|
progressStart: done / total,
|
||||||
|
progressEnd: (done + 1) / total,
|
||||||
|
onProgress: _setProgress,
|
||||||
|
onStatus: _setStatus,
|
||||||
|
);
|
||||||
|
parts.add(summary);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('SyncController: bank tx push skipped ($error)');
|
||||||
|
_pushBankPending = true;
|
||||||
|
}
|
||||||
|
bump();
|
||||||
|
}
|
||||||
|
|
||||||
_setProgress(1.0);
|
_setProgress(1.0);
|
||||||
_lastSyncedAt = DateTime.now();
|
_lastSyncedAt = DateTime.now();
|
||||||
@@ -588,6 +676,7 @@ class SyncController extends ChangeNotifier {
|
|||||||
_uiNotifyThrottle?.cancel();
|
_uiNotifyThrottle?.cancel();
|
||||||
_auth.removeListener(_onAuthChanged);
|
_auth.removeListener(_onAuthChanged);
|
||||||
_bank.onAfterSuccessfulLoad = null;
|
_bank.onAfterSuccessfulLoad = null;
|
||||||
|
_bank.onPendingRemoteDeletesChanged = null;
|
||||||
_meta.onPersisted = null;
|
_meta.onPersisted = null;
|
||||||
_prefs.onFavouritesPersisted = null;
|
_prefs.onFavouritesPersisted = null;
|
||||||
_prefs.onHomeWidgetsPersisted = null;
|
_prefs.onHomeWidgetsPersisted = null;
|
||||||
|
|||||||
@@ -722,6 +722,25 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
_schedulePersist(invalidateLabels: false);
|
_schedulePersist(invalidateLabels: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move per-tx annotations when an admin test transaction's id changes.
|
||||||
|
Future<void> rekeyTransaction(String fromId, String toId) async {
|
||||||
|
if (fromId == toId || fromId.isEmpty || toId.isEmpty) return;
|
||||||
|
final stored = _byId.remove(fromId);
|
||||||
|
if (stored != null) {
|
||||||
|
_byId[toId] = stored;
|
||||||
|
}
|
||||||
|
_labelIdsByTxCache = null;
|
||||||
|
_schedulePersist(invalidateLabels: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop per-tx annotations for a deleted admin test transaction.
|
||||||
|
Future<void> clearTransactionMeta(String txId) async {
|
||||||
|
if (txId.isEmpty) return;
|
||||||
|
if (_byId.remove(txId) == null) return;
|
||||||
|
_labelIdsByTxCache = null;
|
||||||
|
_schedulePersist(invalidateLabels: true);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setNotes(
|
Future<void> setNotes(
|
||||||
BankTransaction tx,
|
BankTransaction tx,
|
||||||
String notes, {
|
String notes, {
|
||||||
@@ -800,6 +819,7 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
List<BankTransaction> txs,
|
List<BankTransaction> txs,
|
||||||
String query, {
|
String query, {
|
||||||
Set<String> labelIds = const {},
|
Set<String> labelIds = const {},
|
||||||
|
LabelMatchMode labelMatch = LabelMatchMode.and,
|
||||||
}) {
|
}) {
|
||||||
final needle = query.trim().toLowerCase();
|
final needle = query.trim().toLowerCase();
|
||||||
final hasText = needle.isNotEmpty;
|
final hasText = needle.isNotEmpty;
|
||||||
@@ -809,7 +829,15 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
final labelIndex = _labelById;
|
final labelIndex = _labelById;
|
||||||
return [
|
return [
|
||||||
for (final tx in txs)
|
for (final tx in txs)
|
||||||
if (_matchesSearch(tx, needle, hasText, labelIds, hasLabels, labelIndex))
|
if (_matchesSearch(
|
||||||
|
tx,
|
||||||
|
needle,
|
||||||
|
hasText,
|
||||||
|
labelIds,
|
||||||
|
hasLabels,
|
||||||
|
labelMatch,
|
||||||
|
labelIndex,
|
||||||
|
))
|
||||||
tx,
|
tx,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -820,10 +848,11 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
bool hasText,
|
bool hasText,
|
||||||
Set<String> labelIds,
|
Set<String> labelIds,
|
||||||
bool hasLabels,
|
bool hasLabels,
|
||||||
|
LabelMatchMode labelMatch,
|
||||||
Map<String, TxLabel> labelIndex,
|
Map<String, TxLabel> labelIndex,
|
||||||
) {
|
) {
|
||||||
final meta = metaFor(tx);
|
final meta = metaFor(tx);
|
||||||
if (hasLabels && !_matchesLabelFilter(meta, labelIds)) {
|
if (hasLabels && !_matchesLabelFilter(meta, labelIds, labelMatch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!hasText) return true;
|
if (!hasText) return true;
|
||||||
@@ -838,20 +867,33 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AND filter: every selected label must be on the tx.
|
/// AND: every selected label must be on the tx.
|
||||||
|
/// OR: at least one selected label (or unlabeled, if that filter is set).
|
||||||
///
|
///
|
||||||
/// Uncategorized alone matches unlabeled txs. Uncategorized combined with
|
/// Uncategorized alone matches unlabeled txs. Uncategorized + others with
|
||||||
/// other labels matches nothing (a tx cannot be both).
|
/// AND matches nothing; with OR matches unlabeled or any of the labels.
|
||||||
bool _matchesLabelFilter(TxMeta meta, Set<String> labelIds) {
|
bool _matchesLabelFilter(
|
||||||
|
TxMeta meta,
|
||||||
|
Set<String> labelIds,
|
||||||
|
LabelMatchMode mode,
|
||||||
|
) {
|
||||||
final wantUncategorized = labelIds.contains(kUncategorizedFilterId);
|
final wantUncategorized = labelIds.contains(kUncategorizedFilterId);
|
||||||
final required = <String>[
|
final required = <String>[
|
||||||
for (final id in labelIds)
|
for (final id in labelIds)
|
||||||
if (id != kUncategorizedFilterId) id,
|
if (id != kUncategorizedFilterId) id,
|
||||||
];
|
];
|
||||||
if (wantUncategorized) {
|
if (required.isEmpty) {
|
||||||
if (required.isNotEmpty) return false;
|
if (wantUncategorized) return !meta.hasLabel;
|
||||||
return !meta.hasLabel;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (mode == LabelMatchMode.or) {
|
||||||
|
if (wantUncategorized && !meta.hasLabel) return true;
|
||||||
|
for (final id in required) {
|
||||||
|
if (meta.labelIds.contains(id)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wantUncategorized) return false;
|
||||||
for (final id in required) {
|
for (final id in required) {
|
||||||
if (!meta.labelIds.contains(id)) return false;
|
if (!meta.labelIds.contains(id)) return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// Signed-in account details, sync status, bank link, and sign-out.
|
/// Signed-in account details, sync status, bank link, and sign-out.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -10,11 +12,26 @@ import '../providers/sync_provider.dart';
|
|||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../utils/connect_bank.dart';
|
import '../utils/connect_bank.dart';
|
||||||
import '../utils/constants.dart';
|
import '../utils/constants.dart';
|
||||||
|
import '../widgets/admin_create_transaction_dialog.dart';
|
||||||
import '../widgets/responsive_body.dart';
|
import '../widgets/responsive_body.dart';
|
||||||
|
|
||||||
class AccountScreen extends StatelessWidget {
|
class AccountScreen extends StatefulWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountScreen> createState() => _AccountScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountScreenState extends State<AccountScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
unawaited(context.read<AuthController>().refreshSession());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final auth = context.watch<AuthController>();
|
final auth = context.watch<AuthController>();
|
||||||
@@ -62,6 +79,24 @@ class AccountScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (auth.isAdmin)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Chip(
|
||||||
|
avatar: Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: colors.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
label: const Text('Admin'),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
backgroundColor: colors.secondaryContainer,
|
||||||
|
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: colors.onSecondaryContainer,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Card(
|
Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -91,6 +126,59 @@ class AccountScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.add_card_outlined,
|
||||||
|
color: auth.isAdmin ? null : colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
title: const Text('Create test transaction'),
|
||||||
|
subtitle: Text(
|
||||||
|
auth.isAdmin
|
||||||
|
? 'Admin · saved locally and synced to PocketBase'
|
||||||
|
: 'Admin only · enable is_admin on your usersapp user in PocketBase, then sign out and back in',
|
||||||
|
),
|
||||||
|
trailing: auth.isAdmin
|
||||||
|
? const Icon(Icons.chevron_right)
|
||||||
|
: Icon(
|
||||||
|
Icons.lock_outline,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
if (!auth.isAdmin) {
|
||||||
|
await auth.refreshSession();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (!context.read<AuthController>().isAdmin) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Not an admin yet. In PocketBase Admin → '
|
||||||
|
'usersapp → your user → set is_admin = true, '
|
||||||
|
'then sign out and sign in again.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final tx = await showAdminCreateTransactionDialog(context);
|
||||||
|
if (tx == null || !context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Created “${tx.description}” · '
|
||||||
|
'${tx.amount.toStringAsFixed(2)} ${tx.currency}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final sync = context.read<SyncController>();
|
||||||
|
if (sync.canSync && !sync.isSyncing) {
|
||||||
|
unawaited(sync.syncNow());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Card(
|
Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import '../providers/preferences_provider.dart';
|
|||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../utils/app_theme.dart';
|
import '../utils/app_theme.dart';
|
||||||
import '../utils/connect_bank.dart';
|
import '../utils/connect_bank.dart';
|
||||||
|
import '../utils/deferred_tab_work.dart';
|
||||||
import '../utils/helpers.dart';
|
import '../utils/helpers.dart';
|
||||||
import '../utils/navigation.dart';
|
import '../utils/navigation.dart';
|
||||||
|
import '../utils/tx_filter_metrics.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/home_filter_widget_card.dart';
|
import '../widgets/home_filter_widget_card.dart';
|
||||||
import '../widgets/home_widgets_sheet.dart';
|
import '../widgets/home_widgets_sheet.dart';
|
||||||
@@ -22,6 +24,18 @@ import '../widgets/skip_build_when_covered.dart';
|
|||||||
import 'transaction_detail_screen.dart';
|
import 'transaction_detail_screen.dart';
|
||||||
import 'transactions_screen.dart';
|
import 'transactions_screen.dart';
|
||||||
|
|
||||||
|
class _HomeSnapshot {
|
||||||
|
const _HomeSnapshot({
|
||||||
|
required this.categorized,
|
||||||
|
required this.statsById,
|
||||||
|
required this.budgetById,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int categorized;
|
||||||
|
final Map<String, TxFilterMetrics> statsById;
|
||||||
|
final Map<String, HomeBudgetMetrics> budgetById;
|
||||||
|
}
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@@ -33,16 +47,93 @@ class HomeScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
var _editingWidgets = false;
|
var _editingWidgets = false;
|
||||||
|
final _work = DeferredTabWork<_HomeSnapshot>();
|
||||||
|
|
||||||
|
int _homeCacheKey({
|
||||||
|
required TxMetaController meta,
|
||||||
|
required BankController bank,
|
||||||
|
required List<HomeFilterWidget> widgets,
|
||||||
|
}) {
|
||||||
|
return Object.hash(
|
||||||
|
meta.storeRevision,
|
||||||
|
identityHashCode(bank.transactions),
|
||||||
|
Object.hashAll(widgets.map((w) => w.id)),
|
||||||
|
Object.hashAll(widgets.map((w) => w.updatedAt)),
|
||||||
|
_editingWidgets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Inactive IndexedStack tabs must not watch providers — otherwise every
|
// Inactive IndexedStack tabs must not watch providers — otherwise every
|
||||||
// annotation save rebuilds Home while the user is on another tab.
|
// annotation save rebuilds Home while the user is on another tab.
|
||||||
if (ShellTabScope.of(context) != HomeScreen.tabIndex) {
|
final active = ShellTabScope.of(context) == HomeScreen.tabIndex;
|
||||||
|
if (!active) {
|
||||||
|
if (_work.isLoading) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ShellTabScope.of(context) == HomeScreen.tabIndex) return;
|
||||||
|
_work.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
return const SizedBox.expand();
|
return const SizedBox.expand();
|
||||||
}
|
}
|
||||||
|
|
||||||
final bank = context.watch<BankController>();
|
final bank = context.watch<BankController>();
|
||||||
|
final meta = context.watch<TxMetaController>();
|
||||||
|
final prefs = context.watch<PreferencesController>();
|
||||||
|
final widgets = _editingWidgets
|
||||||
|
? prefs.homeFilterWidgets
|
||||||
|
: prefs.displayedHomeFilterWidgets;
|
||||||
|
final needsData = bank.isLinked || bank.hasData;
|
||||||
|
final emptyLoading = bank.isLoading && bank.balance == null && !bank.hasData;
|
||||||
|
final cacheKey = _homeCacheKey(meta: meta, bank: bank, widgets: widgets);
|
||||||
|
|
||||||
|
if (needsData && !emptyLoading && !_work.readyFor(cacheKey)) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ShellTabScope.of(context) != HomeScreen.tabIndex) return;
|
||||||
|
_work.ensure(
|
||||||
|
key: cacheKey,
|
||||||
|
mounted: () => mounted,
|
||||||
|
onUpdate: () {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
compute: (yieldUi) async {
|
||||||
|
final categorized =
|
||||||
|
meta.labeledTransactionCount(bank.transactions);
|
||||||
|
await yieldUi();
|
||||||
|
final stats = <String, TxFilterMetrics>{};
|
||||||
|
final budgets = <String, HomeBudgetMetrics>{};
|
||||||
|
for (var i = 0; i < widgets.length; i++) {
|
||||||
|
final w = widgets[i];
|
||||||
|
if (w.type == HomeWidgetType.budget) {
|
||||||
|
budgets[w.id] = computeHomeBudgetMetrics(
|
||||||
|
all: bank.transactions,
|
||||||
|
meta: meta,
|
||||||
|
widget: w,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stats[w.id] = computeTxFilterMetrics(
|
||||||
|
all: bank.transactions,
|
||||||
|
meta: meta,
|
||||||
|
filter: w.filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (i % 2 == 1) await yieldUi();
|
||||||
|
}
|
||||||
|
return _HomeSnapshot(
|
||||||
|
categorized: categorized,
|
||||||
|
statsById: stats,
|
||||||
|
budgetById: budgets,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final showSpinner =
|
||||||
|
needsData && !emptyLoading && !_work.readyFor(cacheKey);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -54,7 +145,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
setState(() => _editingWidgets = !_editingWidgets),
|
setState(() => _editingWidgets = !_editingWidgets),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_editingWidgets ? Icons.check : Icons.dashboard_customize_outlined,
|
_editingWidgets
|
||||||
|
? Icons.check
|
||||||
|
: Icons.dashboard_customize_outlined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...bankAppBarActions(
|
...bankAppBarActions(
|
||||||
@@ -67,9 +160,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: bank.refresh,
|
onRefresh: bank.refresh,
|
||||||
child: ResponsiveBody(
|
child: ResponsiveBody(
|
||||||
child: _HomeBody(
|
child: showSpinner
|
||||||
|
? ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
LoadingView(message: 'Loading home…'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: _HomeBody(
|
||||||
bank: bank,
|
bank: bank,
|
||||||
editingWidgets: _editingWidgets,
|
editingWidgets: _editingWidgets,
|
||||||
|
snapshot: _work.value,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -81,10 +183,12 @@ class _HomeBody extends StatelessWidget {
|
|||||||
const _HomeBody({
|
const _HomeBody({
|
||||||
required this.bank,
|
required this.bank,
|
||||||
required this.editingWidgets,
|
required this.editingWidgets,
|
||||||
|
required this.snapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
final BankController bank;
|
final BankController bank;
|
||||||
final bool editingWidgets;
|
final bool editingWidgets;
|
||||||
|
final _HomeSnapshot? snapshot;
|
||||||
|
|
||||||
void _openTransactions(BuildContext context) {
|
void _openTransactions(BuildContext context) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
@@ -141,7 +245,7 @@ class _HomeBody extends StatelessWidget {
|
|||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
final meta = context.watch<TxMetaController>();
|
final meta = context.watch<TxMetaController>();
|
||||||
final recent = bank.transactions.take(5).toList(growable: false);
|
final recent = bank.transactions.take(5).toList(growable: false);
|
||||||
final categorized =
|
final categorized = snapshot?.categorized ??
|
||||||
meta.labeledTransactionCount(bank.transactions);
|
meta.labeledTransactionCount(bank.transactions);
|
||||||
final widgets = editingWidgets
|
final widgets = editingWidgets
|
||||||
? prefs.homeFilterWidgets
|
? prefs.homeFilterWidgets
|
||||||
@@ -319,6 +423,9 @@ class _HomeBody extends StatelessWidget {
|
|||||||
child: HomeFilterWidgetCard(
|
child: HomeFilterWidgetCard(
|
||||||
widgetConfig: widgetConfig,
|
widgetConfig: widgetConfig,
|
||||||
editing: true,
|
editing: true,
|
||||||
|
precomputedStats: snapshot?.statsById[widgetConfig.id],
|
||||||
|
precomputedBudget:
|
||||||
|
snapshot?.budgetById[widgetConfig.id],
|
||||||
onToggleDisplay: (display) =>
|
onToggleDisplay: (display) =>
|
||||||
prefs.setHomeFilterWidgetDisplay(
|
prefs.setHomeFilterWidgetDisplay(
|
||||||
widgetConfig.id,
|
widgetConfig.id,
|
||||||
@@ -347,7 +454,11 @@ class _HomeBody extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
..._packedHomeWidgetRows(widgets),
|
..._packedHomeWidgetRows(
|
||||||
|
widgets,
|
||||||
|
statsById: snapshot?.statsById,
|
||||||
|
budgetById: snapshot?.budgetById,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
if (_showBuiltIn(
|
if (_showBuiltIn(
|
||||||
editing: editingWidgets,
|
editing: editingWidgets,
|
||||||
@@ -553,7 +664,11 @@ class _HomeWidgetSlot extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
List<Widget> _packedHomeWidgetRows(
|
||||||
|
List<HomeFilterWidget> widgets, {
|
||||||
|
Map<String, TxFilterMetrics>? statsById,
|
||||||
|
Map<String, HomeBudgetMetrics>? budgetById,
|
||||||
|
}) {
|
||||||
final rows = <Widget>[];
|
final rows = <Widget>[];
|
||||||
var i = 0;
|
var i = 0;
|
||||||
while (i < widgets.length) {
|
while (i < widgets.length) {
|
||||||
@@ -571,11 +686,19 @@ List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: HomeFilterWidgetCard(widgetConfig: a),
|
child: HomeFilterWidgetCard(
|
||||||
|
widgetConfig: a,
|
||||||
|
precomputedStats: statsById?[a.id],
|
||||||
|
precomputedBudget: budgetById?[a.id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: HomeFilterWidgetCard(widgetConfig: b),
|
child: HomeFilterWidgetCard(
|
||||||
|
widgetConfig: b,
|
||||||
|
precomputedStats: statsById?[b.id],
|
||||||
|
precomputedBudget: budgetById?[b.id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -590,7 +713,11 @@ List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
|||||||
padding: const EdgeInsets.only(bottom: 10),
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
child: _HomeWidgetSlot(
|
child: _HomeWidgetSlot(
|
||||||
halfWidth: a.halfWidth,
|
halfWidth: a.halfWidth,
|
||||||
child: HomeFilterWidgetCard(widgetConfig: a),
|
child: HomeFilterWidgetCard(
|
||||||
|
widgetConfig: a,
|
||||||
|
precomputedStats: statsById?[a.id],
|
||||||
|
precomputedBudget: budgetById?[a.id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// Look ahead: projected balance from recurring templates through a date.
|
/// Look ahead: projected balance from recurring templates through a date.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -9,13 +11,14 @@ import '../models/transaction_meta.dart';
|
|||||||
import '../providers/bank_provider.dart';
|
import '../providers/bank_provider.dart';
|
||||||
import '../providers/preferences_provider.dart';
|
import '../providers/preferences_provider.dart';
|
||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../screens/transaction_detail_screen.dart';
|
import '../screens/transactions_screen.dart';
|
||||||
import '../utils/app_theme.dart';
|
import '../utils/app_theme.dart';
|
||||||
import '../utils/helpers.dart';
|
import '../utils/helpers.dart';
|
||||||
import '../utils/look_ahead.dart';
|
import '../utils/look_ahead.dart';
|
||||||
import '../utils/navigation.dart';
|
import '../utils/navigation.dart';
|
||||||
import '../utils/tx_calendar.dart';
|
import '../utils/tx_calendar.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
|
import '../widgets/loading_view.dart';
|
||||||
import '../widgets/recurring_template_sheet.dart';
|
import '../widgets/recurring_template_sheet.dart';
|
||||||
import '../widgets/responsive_body.dart';
|
import '../widgets/responsive_body.dart';
|
||||||
import '../widgets/shell_tab_scope.dart';
|
import '../widgets/shell_tab_scope.dart';
|
||||||
@@ -23,6 +26,13 @@ import '../widgets/skip_build_when_covered.dart';
|
|||||||
|
|
||||||
enum _PeriodMode { monthly, yearly }
|
enum _PeriodMode { monthly, yearly }
|
||||||
|
|
||||||
|
class _ItemsSnapshot {
|
||||||
|
const _ItemsSnapshot({required this.key, required this.items});
|
||||||
|
|
||||||
|
final int key;
|
||||||
|
final List<RecurringItem> items;
|
||||||
|
}
|
||||||
|
|
||||||
class LookAheadScreen extends StatefulWidget {
|
class LookAheadScreen extends StatefulWidget {
|
||||||
const LookAheadScreen({super.key});
|
const LookAheadScreen({super.key});
|
||||||
|
|
||||||
@@ -35,8 +45,9 @@ class LookAheadScreen extends StatefulWidget {
|
|||||||
class _LookAheadScreenState extends State<LookAheadScreen> {
|
class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||||
var _period = _PeriodMode.monthly;
|
var _period = _PeriodMode.monthly;
|
||||||
late DateTime _cursor;
|
late DateTime _cursor;
|
||||||
List<RecurringItem>? _cachedItems;
|
_ItemsSnapshot? _snapshot;
|
||||||
int? _itemsCacheKey;
|
int? _loadingKey;
|
||||||
|
int _computeGen = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -129,9 +140,78 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _itemsCacheKey(TxMetaController meta, BankController bank) =>
|
||||||
|
Object.hash(meta.storeRevision, identityHashCode(bank.transactions));
|
||||||
|
|
||||||
|
/// Resolve recurring amounts off the build path, yielding so nav stays live.
|
||||||
|
void _ensureItemsLoaded({
|
||||||
|
required int cacheKey,
|
||||||
|
required TxMetaController meta,
|
||||||
|
required BankController bank,
|
||||||
|
}) {
|
||||||
|
if (_snapshot?.key == cacheKey || _loadingKey == cacheKey) return;
|
||||||
|
_loadingKey = cacheKey;
|
||||||
|
final gen = ++_computeGen;
|
||||||
|
final templates = List<RecurringTemplate>.of(meta.recurringTemplates);
|
||||||
|
final transactions = bank.transactions;
|
||||||
|
|
||||||
|
unawaited(() async {
|
||||||
|
// Let the spinner / tab switch paint before heavy work.
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
if (!mounted || gen != _computeGen) return;
|
||||||
|
|
||||||
|
final items = <RecurringItem>[];
|
||||||
|
for (var i = 0; i < templates.length; i++) {
|
||||||
|
final tpl = templates[i];
|
||||||
|
items.add(
|
||||||
|
RecurringItem(
|
||||||
|
templateId: tpl.id,
|
||||||
|
description: tpl.name,
|
||||||
|
amount: resolveRecurringAmount(
|
||||||
|
template: tpl,
|
||||||
|
transactions: transactions,
|
||||||
|
isInternalTransfer: (tx) => meta.metaFor(tx).internalTransfer,
|
||||||
|
labelIdsFor: meta.labelIdsFor,
|
||||||
|
),
|
||||||
|
currency: tpl.currency,
|
||||||
|
dayOfMonth: tpl.dayOfMonth.clamp(1, 28),
|
||||||
|
ruleSummary: recurringRuleSummary(
|
||||||
|
template: tpl,
|
||||||
|
labelName: (id) => meta.labelById(id)?.name ?? 'label',
|
||||||
|
),
|
||||||
|
sourceTxId: tpl.sourceTxId,
|
||||||
|
labelIds: [
|
||||||
|
if (tpl.labelId != null && tpl.labelId!.isNotEmpty) tpl.labelId!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Yield often so bottom-nav taps are not starved.
|
||||||
|
if (i % 2 == 1) {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
if (!mounted || gen != _computeGen) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted || gen != _computeGen) return;
|
||||||
|
setState(() {
|
||||||
|
_snapshot = _ItemsSnapshot(key: cacheKey, items: items);
|
||||||
|
if (_loadingKey == cacheKey) _loadingKey = null;
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (ShellTabScope.of(context) != LookAheadScreen.tabIndex) {
|
final active = ShellTabScope.of(context) == LookAheadScreen.tabIndex;
|
||||||
|
if (!active) {
|
||||||
|
if (_loadingKey != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ShellTabScope.of(context) == LookAheadScreen.tabIndex) return;
|
||||||
|
_computeGen++;
|
||||||
|
_loadingKey = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
return const SizedBox.expand();
|
return const SizedBox.expand();
|
||||||
}
|
}
|
||||||
return SkipBuildWhenCovered(
|
return SkipBuildWhenCovered(
|
||||||
@@ -165,22 +245,29 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
|||||||
final currentAmount = balance?.amount ?? 0;
|
final currentAmount = balance?.amount ?? 0;
|
||||||
final today = _today;
|
final today = _today;
|
||||||
final through = _through;
|
final through = _through;
|
||||||
final cacheKey = Object.hash(
|
final cacheKey = _itemsCacheKey(meta, bank);
|
||||||
meta.storeRevision,
|
if (_snapshot?.key != cacheKey && _loadingKey != cacheKey) {
|
||||||
identityHashCode(bank.transactions),
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
);
|
if (!mounted) return;
|
||||||
if (_cachedItems == null || _itemsCacheKey != cacheKey) {
|
if (ShellTabScope.of(context) != LookAheadScreen.tabIndex) return;
|
||||||
_itemsCacheKey = cacheKey;
|
_ensureItemsLoaded(cacheKey: cacheKey, meta: meta, bank: bank);
|
||||||
_cachedItems = recurringItemsFromTemplates(
|
});
|
||||||
templates: meta.recurringTemplates,
|
}
|
||||||
transactions: bank.transactions,
|
|
||||||
isInternalTransfer: (tx) => meta.metaFor(tx).internalTransfer,
|
final snapshot = _snapshot;
|
||||||
labelIdsFor: meta.labelIdsFor,
|
final ready = snapshot != null && snapshot.key == cacheKey;
|
||||||
labelName: (id) => meta.labelById(id)?.name ?? 'label',
|
|
||||||
|
if (!ready) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Look ahead'),
|
||||||
|
actions: bankAppBarActions(context: context),
|
||||||
|
),
|
||||||
|
body: const LoadingView(message: 'Calculating look ahead…'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final items = _cachedItems!;
|
|
||||||
|
|
||||||
|
final items = snapshot.items;
|
||||||
final projected = projectedOccurrences(
|
final projected = projectedOccurrences(
|
||||||
items: items,
|
items: items,
|
||||||
from: today,
|
from: today,
|
||||||
@@ -337,17 +424,6 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
items.isEmpty
|
|
||||||
? 'Create a spending from a transaction, or tap New.'
|
|
||||||
: '${items.length} spending${items.length == 1 ? '' : 's'} · '
|
|
||||||
'projected income ${formatMoney(income, currency)} · '
|
|
||||||
'expenses ${formatMoney(expenses, currency)}.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: colors.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -622,6 +698,46 @@ class _TemplateTile extends StatelessWidget {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openTransactions(BuildContext context) {
|
||||||
|
final labelIds = item.labelIds.toSet();
|
||||||
|
SimilarMatchCriteria? similar;
|
||||||
|
TxFlowFilter? flow;
|
||||||
|
String? query;
|
||||||
|
|
||||||
|
if (labelIds.isEmpty) {
|
||||||
|
final tx = _sourceTx;
|
||||||
|
if (tx != null) {
|
||||||
|
// Same matching as Look ahead / apply-to-similar for this source tx.
|
||||||
|
similar = SimilarMatchCriteria(
|
||||||
|
pattern: defaultSimilarPattern(tx.description),
|
||||||
|
flow: tx.amount < 0
|
||||||
|
? TxFlowFilter.expense
|
||||||
|
: tx.amount > 0
|
||||||
|
? TxFlowFilter.income
|
||||||
|
: TxFlowFilter.all,
|
||||||
|
);
|
||||||
|
flow = similar.flow;
|
||||||
|
} else if (template != null && template!.merchantKey.isNotEmpty) {
|
||||||
|
query = template!.merchantKey;
|
||||||
|
} else {
|
||||||
|
query = item.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => TransactionsScreen(
|
||||||
|
initialLabelIds: labelIds,
|
||||||
|
initialLabelMatch: LabelMatchMode.or,
|
||||||
|
initialQuery: query,
|
||||||
|
initialFlow: flow,
|
||||||
|
initialSimilarMatch: similar,
|
||||||
|
initialSearchInMonthOnly: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -658,15 +774,7 @@ class _TemplateTile extends StatelessWidget {
|
|||||||
color: income ? AppTheme.accent : AppTheme.expense,
|
color: income ? AppTheme.accent : AppTheme.expense,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () => _openTransactions(context),
|
||||||
final tx = _sourceTx;
|
|
||||||
if (tx == null) return;
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) => TransactionDetailScreen(tx: tx),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import '../providers/bank_provider.dart';
|
|||||||
import '../providers/preferences_provider.dart';
|
import '../providers/preferences_provider.dart';
|
||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../utils/app_theme.dart';
|
import '../utils/app_theme.dart';
|
||||||
|
import '../utils/deferred_tab_work.dart';
|
||||||
import '../utils/helpers.dart';
|
import '../utils/helpers.dart';
|
||||||
import '../utils/navigation.dart';
|
import '../utils/navigation.dart';
|
||||||
import '../utils/tx_calendar.dart';
|
import '../utils/tx_calendar.dart';
|
||||||
@@ -28,6 +29,36 @@ import 'transactions_screen.dart';
|
|||||||
const _kIncomeColor = AppTheme.accent;
|
const _kIncomeColor = AppTheme.accent;
|
||||||
const _kExpenseColor = AppTheme.expense;
|
const _kExpenseColor = AppTheme.expense;
|
||||||
|
|
||||||
|
class _FavouriteAgg {
|
||||||
|
const _FavouriteAgg({
|
||||||
|
required this.periods,
|
||||||
|
required this.monthly,
|
||||||
|
required this.labelSpend,
|
||||||
|
required this.labelAmountByPeriod,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<DateTime> periods;
|
||||||
|
final Map<DateTime, _MonthBucket> monthly;
|
||||||
|
final List<_LabelSpend> labelSpend;
|
||||||
|
final Map<DateTime, Map<String, double>> labelAmountByPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverviewAggSnapshot {
|
||||||
|
const _OverviewAggSnapshot({
|
||||||
|
required this.periods,
|
||||||
|
required this.monthly,
|
||||||
|
required this.labelSpend,
|
||||||
|
required this.labelAmountByPeriod,
|
||||||
|
required this.favourites,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<DateTime> periods;
|
||||||
|
final Map<DateTime, _MonthBucket> monthly;
|
||||||
|
final List<_LabelSpend> labelSpend;
|
||||||
|
final Map<DateTime, Map<String, double>> labelAmountByPeriod;
|
||||||
|
final Map<String, _FavouriteAgg> favourites;
|
||||||
|
}
|
||||||
|
|
||||||
class OverviewScreen extends StatefulWidget {
|
class OverviewScreen extends StatefulWidget {
|
||||||
const OverviewScreen({super.key});
|
const OverviewScreen({super.key});
|
||||||
|
|
||||||
@@ -59,6 +90,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
/// Favourites charts view (vs full overview with history).
|
/// Favourites charts view (vs full overview with history).
|
||||||
bool _showFavouritesView = false;
|
bool _showFavouritesView = false;
|
||||||
var _favouritesDefaultApplied = false;
|
var _favouritesDefaultApplied = false;
|
||||||
|
final _work = DeferredTabWork<_OverviewAggSnapshot>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -247,7 +279,15 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (ShellTabScope.of(context) != OverviewScreen.tabIndex) {
|
final active = ShellTabScope.of(context) == OverviewScreen.tabIndex;
|
||||||
|
if (!active) {
|
||||||
|
if (_work.isLoading) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ShellTabScope.of(context) == OverviewScreen.tabIndex) return;
|
||||||
|
_work.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
return const SizedBox.expand();
|
return const SizedBox.expand();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +308,41 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final emptyLoading =
|
||||||
|
bank.isLoading && bank.transactions.isEmpty && bank.balance == null;
|
||||||
|
final needsData = bank.isLinked || bank.hasData;
|
||||||
|
final displayedFavs = prefs.displayedOverviewFavourites;
|
||||||
|
final cacheKey = Object.hash(
|
||||||
|
meta.storeRevision,
|
||||||
|
identityHashCode(bank.transactions),
|
||||||
|
_showFavouritesView,
|
||||||
|
_period,
|
||||||
|
_metric,
|
||||||
|
_cursor.year,
|
||||||
|
Object.hashAll(_includedLabelIds),
|
||||||
|
_labelFlow,
|
||||||
|
_labelBarStyle,
|
||||||
|
_labelInvertChart,
|
||||||
|
Object.hashAll(displayedFavs.map((f) => f.id)),
|
||||||
|
Object.hashAll(displayedFavs.map((f) => f.updatedAt)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needsData && !emptyLoading && !_work.readyFor(cacheKey)) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ShellTabScope.of(context) != OverviewScreen.tabIndex) return;
|
||||||
|
_scheduleOverviewCompute(
|
||||||
|
key: cacheKey,
|
||||||
|
bank: bank,
|
||||||
|
meta: meta,
|
||||||
|
displayedFavs: displayedFavs,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final showSpinner =
|
||||||
|
needsData && !emptyLoading && !_work.readyFor(cacheKey);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(_showFavouritesView ? 'Favourites' : 'Overview'),
|
title: Text(_showFavouritesView ? 'Favourites' : 'Overview'),
|
||||||
@@ -288,7 +363,15 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: bank.refresh,
|
onRefresh: bank.refresh,
|
||||||
child: ResponsiveBody(
|
child: ResponsiveBody(
|
||||||
child: SkipBuildWhenCovered(
|
child: showSpinner
|
||||||
|
? ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
LoadingView(message: 'Loading overview…'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: SkipBuildWhenCovered(
|
||||||
builder: (context) => _showFavouritesView
|
builder: (context) => _showFavouritesView
|
||||||
? _buildFavouritesBody(context, bank, meta, prefs)
|
? _buildFavouritesBody(context, bank, meta, prefs)
|
||||||
: _buildBody(context, bank, meta, prefs),
|
: _buildBody(context, bank, meta, prefs),
|
||||||
@@ -298,6 +381,89 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scheduleOverviewCompute({
|
||||||
|
required Object key,
|
||||||
|
required BankController bank,
|
||||||
|
required TxMetaController meta,
|
||||||
|
required List<OverviewFavourite> displayedFavs,
|
||||||
|
}) {
|
||||||
|
_work.ensure(
|
||||||
|
key: key,
|
||||||
|
mounted: () => mounted,
|
||||||
|
onUpdate: () {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
compute: (yieldUi) async {
|
||||||
|
final txs = bank.transactions;
|
||||||
|
final periods = _chartPeriods(txs);
|
||||||
|
await yieldUi();
|
||||||
|
|
||||||
|
Map<DateTime, _MonthBucket> monthly = const {};
|
||||||
|
var labelSpend = const <_LabelSpend>[];
|
||||||
|
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
||||||
|
|
||||||
|
if (!_showFavouritesView) {
|
||||||
|
if (_metric == OverviewMetricMode.flow) {
|
||||||
|
monthly = _flowTotals(txs, meta, periods, _period);
|
||||||
|
} else {
|
||||||
|
final labels = _labelAggregates(
|
||||||
|
txs,
|
||||||
|
meta,
|
||||||
|
periods,
|
||||||
|
_period,
|
||||||
|
includedLabelIds: _includedLabelIds,
|
||||||
|
flow: _labelFlow,
|
||||||
|
);
|
||||||
|
labelSpend = labels.spend;
|
||||||
|
labelAmountByPeriod = labels.amountByPeriod;
|
||||||
|
}
|
||||||
|
await yieldUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
final favourites = <String, _FavouriteAgg>{};
|
||||||
|
if (_showFavouritesView) {
|
||||||
|
for (var i = 0; i < displayedFavs.length; i++) {
|
||||||
|
final favourite = displayedFavs[i];
|
||||||
|
final favPeriods = _chartPeriodsFor(favourite.period, txs);
|
||||||
|
Map<DateTime, _MonthBucket> favMonthly = const {};
|
||||||
|
var favLabels = const <_LabelSpend>[];
|
||||||
|
Map<DateTime, Map<String, double>> favByPeriod = const {};
|
||||||
|
if (favourite.metric == OverviewMetricMode.flow) {
|
||||||
|
favMonthly =
|
||||||
|
_flowTotals(txs, meta, favPeriods, favourite.period);
|
||||||
|
} else {
|
||||||
|
final labels = _labelAggregates(
|
||||||
|
txs,
|
||||||
|
meta,
|
||||||
|
favPeriods,
|
||||||
|
favourite.period,
|
||||||
|
includedLabelIds: favourite.labelIds.toSet(),
|
||||||
|
flow: favourite.flow,
|
||||||
|
);
|
||||||
|
favLabels = labels.spend;
|
||||||
|
favByPeriod = labels.amountByPeriod;
|
||||||
|
}
|
||||||
|
favourites[favourite.id] = _FavouriteAgg(
|
||||||
|
periods: favPeriods,
|
||||||
|
monthly: favMonthly,
|
||||||
|
labelSpend: favLabels,
|
||||||
|
labelAmountByPeriod: favByPeriod,
|
||||||
|
);
|
||||||
|
await yieldUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _OverviewAggSnapshot(
|
||||||
|
periods: periods,
|
||||||
|
monthly: monthly,
|
||||||
|
labelSpend: labelSpend,
|
||||||
|
labelAmountByPeriod: labelAmountByPeriod,
|
||||||
|
favourites: favourites,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFavouritesBody(
|
Widget _buildFavouritesBody(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
BankController bank,
|
BankController bank,
|
||||||
@@ -329,6 +495,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
: 'EUR');
|
: 'EUR');
|
||||||
final txs = bank.transactions;
|
final txs = bank.transactions;
|
||||||
final displayed = prefs.displayedOverviewFavourites;
|
final displayed = prefs.displayedOverviewFavourites;
|
||||||
|
final agg = _work.value;
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
@@ -399,6 +566,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
txs: txs,
|
txs: txs,
|
||||||
meta: meta,
|
meta: meta,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
|
precomputed: agg?.favourites[displayed[i].id],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -412,12 +580,16 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
required List<BankTransaction> txs,
|
required List<BankTransaction> txs,
|
||||||
required TxMetaController meta,
|
required TxMetaController meta,
|
||||||
required String currency,
|
required String currency,
|
||||||
|
_FavouriteAgg? precomputed,
|
||||||
}) {
|
}) {
|
||||||
final periods = _chartPeriodsFor(favourite.period, txs);
|
final periods = precomputed?.periods ??
|
||||||
Map<DateTime, _MonthBucket> monthly = const {};
|
_chartPeriodsFor(favourite.period, txs);
|
||||||
var labelSpend = const <_LabelSpend>[];
|
Map<DateTime, _MonthBucket> monthly = precomputed?.monthly ?? const {};
|
||||||
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
var labelSpend = precomputed?.labelSpend ?? const <_LabelSpend>[];
|
||||||
|
Map<DateTime, Map<String, double>> labelAmountByPeriod =
|
||||||
|
precomputed?.labelAmountByPeriod ?? const {};
|
||||||
|
|
||||||
|
if (precomputed == null) {
|
||||||
if (favourite.metric == OverviewMetricMode.flow) {
|
if (favourite.metric == OverviewMetricMode.flow) {
|
||||||
monthly = _flowTotals(txs, meta, periods, favourite.period);
|
monthly = _flowTotals(txs, meta, periods, favourite.period);
|
||||||
} else {
|
} else {
|
||||||
@@ -432,6 +604,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
labelSpend = labels.spend;
|
labelSpend = labels.spend;
|
||||||
labelAmountByPeriod = labels.amountByPeriod;
|
labelAmountByPeriod = labels.amountByPeriod;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final periodTitle = favourite.period == OverviewPeriodMode.monthly
|
final periodTitle = favourite.period == OverviewPeriodMode.monthly
|
||||||
? '${_cursor.year}'
|
? '${_cursor.year}'
|
||||||
@@ -474,7 +647,29 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onMonthTap: (month) => _openPeriod(context, month),
|
onMonthTap: (month, {labelId, flow}) => _openPeriod(
|
||||||
|
context,
|
||||||
|
month,
|
||||||
|
labelId: labelId,
|
||||||
|
labelIds: {
|
||||||
|
for (final id in favourite.labelIds) id,
|
||||||
|
},
|
||||||
|
flow: flow ??
|
||||||
|
(favourite.metric == OverviewMetricMode.labels
|
||||||
|
? favourite.flow
|
||||||
|
: null),
|
||||||
|
periodMode: favourite.period,
|
||||||
|
),
|
||||||
|
onChartTap: () => _openChartFilter(
|
||||||
|
context,
|
||||||
|
labelIds: {
|
||||||
|
for (final id in favourite.labelIds) id,
|
||||||
|
},
|
||||||
|
flow: favourite.metric == OverviewMetricMode.labels
|
||||||
|
? favourite.flow
|
||||||
|
: null,
|
||||||
|
periodMode: favourite.period,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,15 +703,17 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
? bank.transactions.first.currency
|
? bank.transactions.first.currency
|
||||||
: 'EUR');
|
: 'EUR');
|
||||||
final txs = bank.transactions;
|
final txs = bank.transactions;
|
||||||
final periods = _chartPeriods(txs);
|
final agg = _work.value;
|
||||||
|
final periods = agg?.periods ?? _chartPeriods(txs);
|
||||||
final historyPeriods = [...periods.reversed];
|
final historyPeriods = [...periods.reversed];
|
||||||
|
|
||||||
// Only aggregate what the active metric needs — toggling was scanning
|
// Prefer deferred aggregates; fall back only if snapshot is missing.
|
||||||
// all txs 3–4 times and resolving full meta each time.
|
Map<DateTime, _MonthBucket> monthly = agg?.monthly ?? const {};
|
||||||
Map<DateTime, _MonthBucket> monthly = const {};
|
var labelSpend = agg?.labelSpend ?? const <_LabelSpend>[];
|
||||||
var labelSpend = const <_LabelSpend>[];
|
Map<DateTime, Map<String, double>> labelAmountByPeriod =
|
||||||
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
agg?.labelAmountByPeriod ?? const {};
|
||||||
|
|
||||||
|
if (agg == null) {
|
||||||
if (_metric == OverviewMetricMode.flow) {
|
if (_metric == OverviewMetricMode.flow) {
|
||||||
monthly = _flowTotals(txs, meta, periods, _period);
|
monthly = _flowTotals(txs, meta, periods, _period);
|
||||||
} else {
|
} else {
|
||||||
@@ -531,6 +728,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
labelSpend = labels.spend;
|
labelSpend = labels.spend;
|
||||||
labelAmountByPeriod = labels.amountByPeriod;
|
labelAmountByPeriod = labels.amountByPeriod;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final summaryFlow = _metric == OverviewMetricMode.flow
|
final summaryFlow = _metric == OverviewMetricMode.flow
|
||||||
? _sumBuckets(monthly.values)
|
? _sumBuckets(monthly.values)
|
||||||
@@ -590,7 +788,17 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onMonthTap: (month) => _openPeriod(context, month),
|
onMonthTap: (month, {labelId, flow}) => _openPeriod(
|
||||||
|
context,
|
||||||
|
month,
|
||||||
|
labelId: labelId,
|
||||||
|
flow: flow ??
|
||||||
|
(_metric == OverviewMetricMode.labels ? _labelFlow : null),
|
||||||
|
),
|
||||||
|
onChartTap: () => _openChartFilter(
|
||||||
|
context,
|
||||||
|
flow: _metric == OverviewMetricMode.labels ? _labelFlow : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
if (_metric == OverviewMetricMode.flow) ...[
|
if (_metric == OverviewMetricMode.flow) ...[
|
||||||
@@ -628,11 +836,11 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
currency,
|
currency,
|
||||||
),
|
),
|
||||||
label: switch (_labelFlow) {
|
label: switch (_labelFlow) {
|
||||||
TxFlowFilter.income => 'Labeled income · $_periodTitle',
|
TxFlowFilter.income => 'Income · $_periodTitle',
|
||||||
TxFlowFilter.expense => 'Labeled spend · $_periodTitle',
|
TxFlowFilter.expense => 'Spend · $_periodTitle',
|
||||||
TxFlowFilter.all => _labelInvertChart
|
TxFlowFilter.all => _labelInvertChart
|
||||||
? 'Labeled net (expense view) · $_periodTitle'
|
? 'Net (expense view) · $_periodTitle'
|
||||||
: 'Labeled net · $_periodTitle',
|
: 'Net · $_periodTitle',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -739,22 +947,60 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
DateTime period, {
|
DateTime period, {
|
||||||
String? labelId,
|
String? labelId,
|
||||||
|
Set<String>? labelIds,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
OverviewPeriodMode? periodMode,
|
||||||
}) {
|
}) {
|
||||||
final yearly = _period == OverviewPeriodMode.yearly;
|
final mode = periodMode ?? _period;
|
||||||
|
final yearly = mode == OverviewPeriodMode.yearly;
|
||||||
final initial = yearly ? DateTime(period.year, 12) : period;
|
final initial = yearly ? DateTime(period.year, 12) : period;
|
||||||
final labelIds = labelId != null
|
final resolvedLabelIds = labelId != null
|
||||||
? {labelId}
|
? {labelId}
|
||||||
: {
|
: (labelIds ??
|
||||||
|
(_metric == OverviewMetricMode.labels
|
||||||
|
? {
|
||||||
// Open with the same labels currently shown on the chart.
|
// Open with the same labels currently shown on the chart.
|
||||||
for (final id in _includedLabelIds) id,
|
for (final id in _includedLabelIds) id,
|
||||||
|
}
|
||||||
|
: <String>{}));
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => TransactionsScreen(
|
||||||
|
initialMonth: initial,
|
||||||
|
initialLabelIds: resolvedLabelIds,
|
||||||
|
initialLabelMatch: LabelMatchMode.or,
|
||||||
|
initialFlow: flow,
|
||||||
|
// A specific bar/period should always scope to that month (or year).
|
||||||
|
initialSearchInMonthOnly: !yearly,
|
||||||
|
initialYear: yearly ? period.year : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open Transactions with this chart's filters (no specific bar/column).
|
||||||
|
void _openChartFilter(
|
||||||
|
BuildContext context, {
|
||||||
|
Set<String>? labelIds,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
OverviewPeriodMode? periodMode,
|
||||||
|
}) {
|
||||||
|
final mode = periodMode ?? _period;
|
||||||
|
final yearly = mode == OverviewPeriodMode.yearly;
|
||||||
|
final resolvedLabelIds = labelIds ??
|
||||||
|
{
|
||||||
|
for (final id in _includedLabelIds) id,
|
||||||
};
|
};
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder: (_) => TransactionsScreen(
|
builder: (_) => TransactionsScreen(
|
||||||
initialMonth: initial,
|
// Monthly charts show a year — open that whole year with the chart filter.
|
||||||
initialLabelIds: labelIds,
|
initialMonth: yearly ? null : DateTime(_cursor.year, 12),
|
||||||
initialSearchInMonthOnly: !yearly && labelIds.isNotEmpty,
|
initialYear: yearly ? null : _cursor.year,
|
||||||
initialYear: yearly ? period.year : null,
|
initialLabelIds: resolvedLabelIds,
|
||||||
|
initialLabelMatch: LabelMatchMode.or,
|
||||||
|
initialFlow: flow,
|
||||||
|
initialSearchInMonthOnly: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1158,6 +1404,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
required this.availableLabelCount,
|
required this.availableLabelCount,
|
||||||
required this.onLabelTap,
|
required this.onLabelTap,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
this.onPeriodModeChanged,
|
this.onPeriodModeChanged,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
});
|
});
|
||||||
@@ -1181,7 +1428,12 @@ class _ChartCard extends StatelessWidget {
|
|||||||
final int? includedLabelCount;
|
final int? includedLabelCount;
|
||||||
final int availableLabelCount;
|
final int availableLabelCount;
|
||||||
final void Function(TxLabel label) onLabelTap;
|
final void Function(TxLabel label) onLabelTap;
|
||||||
final void Function(DateTime month) onMonthTap;
|
final void Function(
|
||||||
|
DateTime month, {
|
||||||
|
String? labelId,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged;
|
final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged;
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
@@ -1276,7 +1528,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
runSpacing: 2,
|
runSpacing: 2,
|
||||||
alignment: WrapAlignment.end,
|
alignment: WrapAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
for (final row in labelSpend.take(compact ? 3 : 5))
|
for (final row in labelSpend.take(6))
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onLabelTap(row.label),
|
onTap: () => onLabelTap(row.label),
|
||||||
child: _LegendDot(
|
child: _LegendDot(
|
||||||
@@ -1285,7 +1537,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
compact: compact,
|
compact: compact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (labelSpend.length > (compact ? 3 : 5))
|
if (labelSpend.length > 6)
|
||||||
_LegendDot(
|
_LegendDot(
|
||||||
color: colors.outline,
|
color: colors.outline,
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
@@ -1358,13 +1610,15 @@ class _ChartCard extends StatelessWidget {
|
|||||||
final hasLegend = metric == OverviewMetricMode.flow ||
|
final hasLegend = metric == OverviewMetricMode.flow ||
|
||||||
labelSpend.isNotEmpty;
|
labelSpend.isNotEmpty;
|
||||||
|
|
||||||
|
final radius = BorderRadius.circular(compact ? 16 : 20);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
color: colors.surfaceContainer,
|
color: colors.surfaceContainer,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
clipBehavior: Clip.antiAlias,
|
||||||
borderRadius: BorderRadius.circular(compact ? 16 : 20),
|
shape: RoundedRectangleBorder(borderRadius: radius),
|
||||||
),
|
// Header/legend open the chart-wide filter; bars handle their own taps.
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
compact ? 12 : 14,
|
compact ? 12 : 14,
|
||||||
@@ -1372,6 +1626,13 @@ class _ChartCard extends StatelessWidget {
|
|||||||
compact ? 10 : 14,
|
compact ? 10 : 14,
|
||||||
compact ? 12 : 16,
|
compact ? 12 : 16,
|
||||||
),
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onChartTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (compact) ...[
|
if (compact) ...[
|
||||||
@@ -1520,6 +1781,10 @@ class _ChartCard extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
child: metric == OverviewMetricMode.flow
|
child: metric == OverviewMetricMode.flow
|
||||||
@@ -1528,6 +1793,8 @@ class _ChartCard extends StatelessWidget {
|
|||||||
monthly: monthly,
|
monthly: monthly,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
period: period,
|
period: period,
|
||||||
|
onMonthTap: onMonthTap,
|
||||||
|
onChartTap: onChartTap,
|
||||||
)
|
)
|
||||||
: _LabelMonthBars(
|
: _LabelMonthBars(
|
||||||
months: months,
|
months: months,
|
||||||
@@ -1538,6 +1805,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
barStyle: labelBarStyle,
|
barStyle: labelBarStyle,
|
||||||
invertChart: invertChart,
|
invertChart: invertChart,
|
||||||
onMonthTap: onMonthTap,
|
onMonthTap: onMonthTap,
|
||||||
|
onChartTap: onChartTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1553,12 +1821,20 @@ class _FlowBars extends StatelessWidget {
|
|||||||
required this.monthly,
|
required this.monthly,
|
||||||
required this.currency,
|
required this.currency,
|
||||||
required this.period,
|
required this.period,
|
||||||
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<DateTime> months;
|
final List<DateTime> months;
|
||||||
final Map<DateTime, _MonthBucket> monthly;
|
final Map<DateTime, _MonthBucket> monthly;
|
||||||
final String currency;
|
final String currency;
|
||||||
final OverviewPeriodMode period;
|
final OverviewPeriodMode period;
|
||||||
|
final void Function(
|
||||||
|
DateTime month, {
|
||||||
|
String? labelId,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1577,7 +1853,10 @@ class _FlowBars extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
period == OverviewPeriodMode.yearly
|
period == OverviewPeriodMode.yearly
|
||||||
? 'No income or expenses in these years'
|
? 'No income or expenses in these years'
|
||||||
@@ -1585,6 +1864,7 @@ class _FlowBars extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1593,23 +1873,35 @@ class _FlowBars extends StatelessWidget {
|
|||||||
maxY: safeMax,
|
maxY: safeMax,
|
||||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||||
barTouchData: BarTouchData(
|
barTouchData: BarTouchData(
|
||||||
|
handleBuiltInTouches: false,
|
||||||
touchTooltipData: BarTouchTooltipData(
|
touchTooltipData: BarTouchTooltipData(
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
getTooltipItem: (group, groupIndex, rod, rodIndex) => null,
|
||||||
final month = months[group.x.toInt()];
|
),
|
||||||
final bucket = monthly[_periodKey(month, period)]!;
|
touchCallback: (event, response) {
|
||||||
final isIncome = rodIndex == 1;
|
if (event is! FlTapUpEvent) return;
|
||||||
final value = isIncome ? bucket.income : bucket.expense;
|
final spot = response?.spot;
|
||||||
return BarTooltipItem(
|
if (spot == null) {
|
||||||
'${_periodAxisLabel(month, period)}\n'
|
onChartTap();
|
||||||
'${isIncome ? 'Income' : 'Expense'}\n'
|
return;
|
||||||
'${formatMoney(value, currency)}',
|
}
|
||||||
TextStyle(color: colors.onInverseSurface),
|
final index = spot.touchedBarGroupIndex;
|
||||||
);
|
if (index < 0 || index >= months.length) {
|
||||||
|
onChartTap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final rod = spot.touchedRodDataIndex;
|
||||||
|
// Rod 0 = expense, rod 1 = income (see barGroups below).
|
||||||
|
final flow = rod == 0
|
||||||
|
? TxFlowFilter.expense
|
||||||
|
: rod == 1
|
||||||
|
? TxFlowFilter.income
|
||||||
|
: null;
|
||||||
|
onMonthTap(months[index], flow: flow);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
topTitles:
|
||||||
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
rightTitles:
|
rightTitles:
|
||||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
leftTitles: AxisTitles(
|
leftTitles: AxisTitles(
|
||||||
@@ -1706,9 +1998,10 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
required this.barStyle,
|
required this.barStyle,
|
||||||
required this.invertChart,
|
required this.invertChart,
|
||||||
required this.onMonthTap,
|
required this.onMonthTap,
|
||||||
|
required this.onChartTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const _maxSeriesLabels = 5;
|
static const _maxSeriesLabels = 6;
|
||||||
|
|
||||||
final List<DateTime> months;
|
final List<DateTime> months;
|
||||||
final List<_LabelSpend> labelSpend;
|
final List<_LabelSpend> labelSpend;
|
||||||
@@ -1717,7 +2010,12 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
final OverviewPeriodMode period;
|
final OverviewPeriodMode period;
|
||||||
final LabelChartBarStyle barStyle;
|
final LabelChartBarStyle barStyle;
|
||||||
final bool invertChart;
|
final bool invertChart;
|
||||||
final void Function(DateTime month) onMonthTap;
|
final void Function(
|
||||||
|
DateTime month, {
|
||||||
|
String? labelId,
|
||||||
|
TxFlowFilter? flow,
|
||||||
|
}) onMonthTap;
|
||||||
|
final VoidCallback onChartTap;
|
||||||
|
|
||||||
double _plot(double value) => invertChart ? -value : value;
|
double _plot(double value) => invertChart ? -value : value;
|
||||||
|
|
||||||
@@ -1726,12 +2024,16 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
if (labelSpend.isEmpty) {
|
if (labelSpend.isEmpty) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No labeled activity in this period.\nTag transactions to see them here.',
|
'No labeled activity in this period.\nTag transactions to see them here.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1788,12 +2090,16 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (maxY == 0 && minY == 0) {
|
if (maxY == 0 && minY == 0) {
|
||||||
return Center(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: onChartTap,
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No labeled activity in this period.\nTag transactions to see them here.',
|
'No labeled activity in this period.\nTag transactions to see them here.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final span = (maxY - minY).abs();
|
final span = (maxY - minY).abs();
|
||||||
@@ -1804,12 +2110,12 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
final rodWidth = stacked
|
final rodWidth = stacked
|
||||||
? (period == OverviewPeriodMode.yearly ? 18.0 : 14.0)
|
? (period == OverviewPeriodMode.yearly ? 18.0 : 14.0)
|
||||||
: period == OverviewPeriodMode.yearly
|
: period == OverviewPeriodMode.yearly
|
||||||
? (seriesCount <= 3 ? 12.0 : 8.0)
|
? (seriesCount <= 3 ? 12.0 : seriesCount <= 6 ? 8.0 : 6.0)
|
||||||
: (seriesCount <= 3
|
: (seriesCount <= 3
|
||||||
? 7.0
|
? 7.0
|
||||||
: seriesCount <= 5
|
: seriesCount <= 6
|
||||||
? 5.0
|
? 4.5
|
||||||
: 4.0);
|
: 3.5);
|
||||||
|
|
||||||
List<BarChartRodData> rodsFor(DateTime month) {
|
List<BarChartRodData> rodsFor(DateTime month) {
|
||||||
final bucket =
|
final bucket =
|
||||||
@@ -1893,52 +2199,40 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
maxY: safeMax,
|
maxY: safeMax,
|
||||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||||
barTouchData: BarTouchData(
|
barTouchData: BarTouchData(
|
||||||
|
handleBuiltInTouches: false,
|
||||||
|
touchTooltipData: BarTouchTooltipData(
|
||||||
|
getTooltipItem: (group, groupIndex, rod, rodIndex) => null,
|
||||||
|
),
|
||||||
touchCallback: (event, response) {
|
touchCallback: (event, response) {
|
||||||
if (event is! FlTapUpEvent) return;
|
if (event is! FlTapUpEvent) return;
|
||||||
final index = response?.spot?.touchedBarGroupIndex;
|
final spot = response?.spot;
|
||||||
if (index == null || index < 0 || index >= months.length) return;
|
if (spot == null) {
|
||||||
onMonthTap(months[index]);
|
onChartTap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final index = spot.touchedBarGroupIndex;
|
||||||
|
if (index < 0 || index >= months.length) {
|
||||||
|
onChartTap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final month = months[index];
|
||||||
|
if (stacked) {
|
||||||
|
final stackIndex = spot.touchedStackItemIndex;
|
||||||
|
if (stackIndex >= 0 && stackIndex < topLabels.length) {
|
||||||
|
onMonthTap(month, labelId: topLabels[stackIndex].label.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// "Other" segment or whole column → that period with chart labels.
|
||||||
|
onMonthTap(month);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final rod = spot.touchedRodDataIndex;
|
||||||
|
if (rod >= 0 && rod < topLabels.length) {
|
||||||
|
onMonthTap(month, labelId: topLabels[rod].label.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onMonthTap(month);
|
||||||
},
|
},
|
||||||
touchTooltipData: BarTouchTooltipData(
|
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
||||||
final month = months[group.x.toInt()];
|
|
||||||
if (stacked && rod.rodStackItems.isNotEmpty) {
|
|
||||||
final bucket =
|
|
||||||
amountByMonth[_periodKey(month, period)] ?? const {};
|
|
||||||
final buffer = StringBuffer(
|
|
||||||
'${_periodAxisLabel(month, period)}\n',
|
|
||||||
);
|
|
||||||
var total = 0.0;
|
|
||||||
for (final row in topLabels) {
|
|
||||||
final amount = bucket[row.label.id] ?? 0;
|
|
||||||
if (amount == 0) continue;
|
|
||||||
total += amount;
|
|
||||||
buffer.writeln(
|
|
||||||
'${row.label.name}: ${formatMoney(amount, currency)}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final other = otherFor(month);
|
|
||||||
if (other != 0) {
|
|
||||||
total += other;
|
|
||||||
buffer.writeln('Other: ${formatMoney(other, currency)}');
|
|
||||||
}
|
|
||||||
buffer.write('Total: ${formatMoney(total, currency)}');
|
|
||||||
return BarTooltipItem(
|
|
||||||
buffer.toString(),
|
|
||||||
TextStyle(color: colors.onInverseSurface),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final name = rodIndex < topLabels.length
|
|
||||||
? topLabels[rodIndex].label.name
|
|
||||||
: 'Other';
|
|
||||||
return BarTooltipItem(
|
|
||||||
'${_periodAxisLabel(month, period)}\n'
|
|
||||||
'$name\n'
|
|
||||||
'${formatMoney(invertChart ? -rod.toY : rod.toY, currency)}',
|
|
||||||
TextStyle(color: colors.onInverseSurface),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../models/bank.dart';
|
import '../models/bank.dart';
|
||||||
import '../models/transaction_meta.dart';
|
import '../models/transaction_meta.dart';
|
||||||
|
import '../providers/auth_provider.dart';
|
||||||
import '../providers/bank_provider.dart';
|
import '../providers/bank_provider.dart';
|
||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../utils/app_theme.dart';
|
import '../utils/app_theme.dart';
|
||||||
import '../utils/helpers.dart';
|
import '../utils/helpers.dart';
|
||||||
|
import '../widgets/admin_create_transaction_dialog.dart';
|
||||||
import '../widgets/label_parent_picker.dart';
|
import '../widgets/label_parent_picker.dart';
|
||||||
import '../widgets/recurring_template_sheet.dart';
|
import '../widgets/recurring_template_sheet.dart';
|
||||||
import '../widgets/responsive_body.dart';
|
import '../widgets/responsive_body.dart';
|
||||||
@@ -26,6 +28,7 @@ class TransactionDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
||||||
|
late BankTransaction _tx;
|
||||||
late final TextEditingController _notesController;
|
late final TextEditingController _notesController;
|
||||||
late final TextEditingController _labelSearchController;
|
late final TextEditingController _labelSearchController;
|
||||||
late SimilarMatchCriteria _matchCriteria;
|
late SimilarMatchCriteria _matchCriteria;
|
||||||
@@ -41,11 +44,12 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_tx = widget.tx;
|
||||||
final meta = context.read<TxMetaController>();
|
final meta = context.read<TxMetaController>();
|
||||||
final notes = meta.metaFor(widget.tx).notes;
|
final notes = meta.metaFor(_tx).notes;
|
||||||
_notesController = TextEditingController(text: notes);
|
_notesController = TextEditingController(text: notes);
|
||||||
_labelSearchController = TextEditingController();
|
_labelSearchController = TextEditingController();
|
||||||
final tx = widget.tx;
|
final tx = _tx;
|
||||||
_matchCriteria = SimilarMatchCriteria(
|
_matchCriteria = SimilarMatchCriteria(
|
||||||
pattern: defaultSimilarPattern(tx.description),
|
pattern: defaultSimilarPattern(tx.description),
|
||||||
flow: tx.amount < 0
|
flow: tx.amount < 0
|
||||||
@@ -63,7 +67,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final all = context.read<BankController>().transactions;
|
final all = context.read<BankController>().transactions;
|
||||||
final count = context.read<TxMetaController>().countSimilar(
|
final count = context.read<TxMetaController>().countSimilar(
|
||||||
widget.tx,
|
_tx,
|
||||||
all,
|
all,
|
||||||
criteria: _matchCriteria,
|
criteria: _matchCriteria,
|
||||||
);
|
);
|
||||||
@@ -88,14 +92,14 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
List<BankTransaction> _similarScope(TxMetaController meta) {
|
List<BankTransaction> _similarScope(TxMetaController meta) {
|
||||||
final all = context.read<BankController>().transactions;
|
final all = context.read<BankController>().transactions;
|
||||||
if (!_applyToSimilar) return const [];
|
if (!_applyToSimilar) return const [];
|
||||||
return meta.similarTo(widget.tx, all, criteria: _matchCriteria);
|
return meta.similarTo(_tx, all, criteria: _matchCriteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editMatchCriteria() async {
|
Future<void> _editMatchCriteria() async {
|
||||||
final all = context.read<BankController>().transactions;
|
final all = context.read<BankController>().transactions;
|
||||||
final result = await showSimilarMatchDialog(
|
final result = await showSimilarMatchDialog(
|
||||||
context,
|
context,
|
||||||
prototype: widget.tx,
|
prototype: _tx,
|
||||||
all: all,
|
all: all,
|
||||||
initial: _matchCriteria,
|
initial: _matchCriteria,
|
||||||
);
|
);
|
||||||
@@ -138,7 +142,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
final similar = _similarScope(meta);
|
final similar = _similarScope(meta);
|
||||||
final count = similar.isEmpty ? 1 : similar.length;
|
final count = similar.isEmpty ? 1 : similar.length;
|
||||||
await meta.setNotes(
|
await meta.setNotes(
|
||||||
widget.tx,
|
_tx,
|
||||||
_notesController.text,
|
_notesController.text,
|
||||||
applyToSimilar: similar,
|
applyToSimilar: similar,
|
||||||
matchPattern: _pattern,
|
matchPattern: _pattern,
|
||||||
@@ -180,7 +184,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
() async {
|
() async {
|
||||||
final meta = context.read<TxMetaController>();
|
final meta = context.read<TxMetaController>();
|
||||||
final similar = toMatched ? _similarScope(meta) : const <BankTransaction>[];
|
final similar = toMatched ? _similarScope(meta) : const <BankTransaction>[];
|
||||||
final targets = similar.isEmpty ? [widget.tx] : similar;
|
final targets = similar.isEmpty ? [_tx] : similar;
|
||||||
await meta.setLabelsForMany(
|
await meta.setLabelsForMany(
|
||||||
targets,
|
targets,
|
||||||
_draftLabelIds.toList(growable: false),
|
_draftLabelIds.toList(growable: false),
|
||||||
@@ -188,7 +192,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_labelsDirty = false;
|
_labelsDirty = false;
|
||||||
_draftLabelIds = meta.metaFor(widget.tx).labelIds.toSet();
|
_draftLabelIds = meta.metaFor(_tx).labelIds.toSet();
|
||||||
});
|
});
|
||||||
final count = targets.length;
|
final count = targets.length;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -291,9 +295,79 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
await context.read<TxMetaController>().removeLabel(label.id);
|
await context.read<TxMetaController>().removeLabel(label.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _editManualTransaction() async {
|
||||||
|
final updated = await showAdminCreateTransactionDialog(
|
||||||
|
context,
|
||||||
|
existing: _tx,
|
||||||
|
);
|
||||||
|
if (updated == null || !mounted) return;
|
||||||
|
final meta = context.read<TxMetaController>();
|
||||||
|
if (updated.id != _tx.id) {
|
||||||
|
await meta.rekeyTransaction(_tx.id, updated.id);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_tx = updated;
|
||||||
|
_matchCriteria = SimilarMatchCriteria(
|
||||||
|
pattern: defaultSimilarPattern(updated.description),
|
||||||
|
flow: updated.amount < 0
|
||||||
|
? TxFlowFilter.expense
|
||||||
|
: updated.amount > 0
|
||||||
|
? TxFlowFilter.income
|
||||||
|
: TxFlowFilter.all,
|
||||||
|
);
|
||||||
|
_draftLabelIds = meta.metaFor(updated).labelIds.toSet();
|
||||||
|
_labelsDirty = false;
|
||||||
|
_similarReady = false;
|
||||||
|
_similarCount = 0;
|
||||||
|
});
|
||||||
|
_refreshSimilarCount();
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Test transaction updated')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteManualTransaction() async {
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Delete test transaction?'),
|
||||||
|
content: const Text(
|
||||||
|
'This removes the transaction from this device. The remote copy is '
|
||||||
|
'deleted on the next PocketBase sync.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (ok != true || !mounted) return;
|
||||||
|
final id = _tx.id;
|
||||||
|
final bank = context.read<BankController>();
|
||||||
|
final meta = context.read<TxMetaController>();
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
await bank.deleteManualTransaction(_tx);
|
||||||
|
await meta.clearTransactionMeta(id);
|
||||||
|
if (!mounted) return;
|
||||||
|
navigator.pop();
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(content: Text('Test transaction deleted')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tx = widget.tx;
|
final tx = _tx;
|
||||||
final metaController = context.watch<TxMetaController>();
|
final metaController = context.watch<TxMetaController>();
|
||||||
final meta = metaController.metaFor(tx);
|
final meta = metaController.metaFor(tx);
|
||||||
final similarCount = _similarCount;
|
final similarCount = _similarCount;
|
||||||
@@ -317,11 +391,37 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
if (keep.contains(entry.label.id)) entry,
|
if (keep.contains(entry.label.id)) entry,
|
||||||
];
|
];
|
||||||
}();
|
}();
|
||||||
|
final canEditManual =
|
||||||
|
context.watch<AuthController>().isAdmin && tx.isManual;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Transaction'),
|
title: const Text('Transaction'),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (canEditManual)
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
enabled: !_busy,
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'edit') {
|
||||||
|
_editManualTransaction();
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
_deleteManualTransaction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Text('Edit test transaction'),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Text(
|
||||||
|
'Delete',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
if (_dirty)
|
if (_dirty)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _busy ? null : _saveNotes,
|
onPressed: _busy ? null : _saveNotes,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:provider/provider.dart';
|
|||||||
import '../models/bank.dart';
|
import '../models/bank.dart';
|
||||||
import '../models/transaction_meta.dart';
|
import '../models/transaction_meta.dart';
|
||||||
import '../providers/bank_provider.dart';
|
import '../providers/bank_provider.dart';
|
||||||
import '../providers/preferences_provider.dart';
|
|
||||||
import '../providers/tx_meta_provider.dart';
|
import '../providers/tx_meta_provider.dart';
|
||||||
import '../utils/app_theme.dart';
|
import '../utils/app_theme.dart';
|
||||||
import '../utils/connect_bank.dart';
|
import '../utils/connect_bank.dart';
|
||||||
@@ -17,7 +16,6 @@ import '../utils/helpers.dart';
|
|||||||
import '../utils/navigation.dart';
|
import '../utils/navigation.dart';
|
||||||
import '../utils/tx_calendar.dart';
|
import '../utils/tx_calendar.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/home_widgets_sheet.dart';
|
|
||||||
import '../widgets/label_filter_dialog.dart';
|
import '../widgets/label_filter_dialog.dart';
|
||||||
import '../widgets/loading_view.dart';
|
import '../widgets/loading_view.dart';
|
||||||
import '../widgets/responsive_body.dart';
|
import '../widgets/responsive_body.dart';
|
||||||
@@ -30,11 +28,13 @@ class TransactionsScreen extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.initialMonth,
|
this.initialMonth,
|
||||||
this.initialLabelIds = const {},
|
this.initialLabelIds = const {},
|
||||||
|
this.initialLabelMatch = LabelMatchMode.and,
|
||||||
this.initialSearchInMonthOnly,
|
this.initialSearchInMonthOnly,
|
||||||
this.initialYear,
|
this.initialYear,
|
||||||
this.initialQuery,
|
this.initialQuery,
|
||||||
this.initialFlow,
|
this.initialFlow,
|
||||||
this.initialAmountRange,
|
this.initialAmountRange,
|
||||||
|
this.initialSimilarMatch,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// When set, opens on this calendar month instead of the current month.
|
/// When set, opens on this calendar month instead of the current month.
|
||||||
@@ -43,6 +43,9 @@ class TransactionsScreen extends StatefulWidget {
|
|||||||
/// Pre-select these label filters (e.g. from overview history).
|
/// Pre-select these label filters (e.g. from overview history).
|
||||||
final Set<String> initialLabelIds;
|
final Set<String> initialLabelIds;
|
||||||
|
|
||||||
|
/// AND vs OR for [initialLabelIds] (and later filter edits).
|
||||||
|
final LabelMatchMode initialLabelMatch;
|
||||||
|
|
||||||
/// When set, overrides the default "search in month" checkbox.
|
/// When set, overrides the default "search in month" checkbox.
|
||||||
final bool? initialSearchInMonthOnly;
|
final bool? initialSearchInMonthOnly;
|
||||||
|
|
||||||
@@ -55,6 +58,9 @@ class TransactionsScreen extends StatefulWidget {
|
|||||||
final TxFlowFilter? initialFlow;
|
final TxFlowFilter? initialFlow;
|
||||||
final TxAmountRange? initialAmountRange;
|
final TxAmountRange? initialAmountRange;
|
||||||
|
|
||||||
|
/// Match txs the same way as “apply to similar” / Look ahead transaction source.
|
||||||
|
final SimilarMatchCriteria? initialSimilarMatch;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TransactionsScreen> createState() => _TransactionsScreenState();
|
State<TransactionsScreen> createState() => _TransactionsScreenState();
|
||||||
}
|
}
|
||||||
@@ -68,6 +74,8 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
DateTime? _selectedDay;
|
DateTime? _selectedDay;
|
||||||
late bool _searchInMonthOnly;
|
late bool _searchInMonthOnly;
|
||||||
late final Set<String> _labelFilters;
|
late final Set<String> _labelFilters;
|
||||||
|
late LabelMatchMode _labelMatch;
|
||||||
|
SimilarMatchCriteria? _similarMatch;
|
||||||
int? _yearFilter;
|
int? _yearFilter;
|
||||||
var _showCalendar = false;
|
var _showCalendar = false;
|
||||||
var _flowFilter = TxFlowFilter.all;
|
var _flowFilter = TxFlowFilter.all;
|
||||||
@@ -89,6 +97,8 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
_month = start.isAfter(now) ? now : start;
|
_month = start.isAfter(now) ? now : start;
|
||||||
}
|
}
|
||||||
_labelFilters = Set<String>.of(widget.initialLabelIds);
|
_labelFilters = Set<String>.of(widget.initialLabelIds);
|
||||||
|
_labelMatch = widget.initialLabelMatch;
|
||||||
|
_similarMatch = widget.initialSimilarMatch;
|
||||||
// Default to current month so the list, navigator, and filter popup agree.
|
// Default to current month so the list, navigator, and filter popup agree.
|
||||||
_searchInMonthOnly = widget.initialSearchInMonthOnly ?? true;
|
_searchInMonthOnly = widget.initialSearchInMonthOnly ?? true;
|
||||||
_yearFilter = widget.initialYear;
|
_yearFilter = widget.initialYear;
|
||||||
@@ -98,6 +108,11 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
if (initialQuery.isNotEmpty) {
|
if (initialQuery.isNotEmpty) {
|
||||||
_searchController.text = initialQuery;
|
_searchController.text = initialQuery;
|
||||||
_activeQuery = initialQuery;
|
_activeQuery = initialQuery;
|
||||||
|
} else if (_similarMatch != null &&
|
||||||
|
_similarMatch!.pattern.trim().isNotEmpty) {
|
||||||
|
// Show the similar pattern in the search field for context; filtering
|
||||||
|
// uses [txMatchesSimilarCriteria], not plain text contains.
|
||||||
|
_searchController.text = _similarMatch!.pattern;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,16 +124,23 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get _isSearching =>
|
bool get _isSearching =>
|
||||||
_activeQuery.trim().isNotEmpty || _labelFilters.isNotEmpty;
|
_activeQuery.trim().isNotEmpty ||
|
||||||
|
_labelFilters.isNotEmpty ||
|
||||||
|
_similarMatch != null;
|
||||||
|
|
||||||
bool get _hasListFilters =>
|
bool get _hasListFilters =>
|
||||||
_labelFilters.isNotEmpty ||
|
_labelFilters.isNotEmpty ||
|
||||||
_flowFilter != TxFlowFilter.all ||
|
_flowFilter != TxFlowFilter.all ||
|
||||||
_amountRange.isActive;
|
_amountRange.isActive ||
|
||||||
|
_similarMatch != null;
|
||||||
|
|
||||||
bool _matchesExtraFilters(BankTransaction tx, TxMetaController meta) {
|
bool _matchesExtraFilters(BankTransaction tx, TxMetaController meta) {
|
||||||
if (!txMatchesFlow(tx, _flowFilter)) return false;
|
if (!txMatchesFlow(tx, _flowFilter)) return false;
|
||||||
if (!txMatchesAmountRange(tx, _amountRange)) return false;
|
if (!txMatchesAmountRange(tx, _amountRange)) return false;
|
||||||
|
final similar = _similarMatch;
|
||||||
|
if (similar != null && !txMatchesSimilarCriteria(tx, similar)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +169,11 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
_searchDebounceTimer?.cancel();
|
_searchDebounceTimer?.cancel();
|
||||||
|
|
||||||
|
// Editing the box leaves “similar match” mode and uses plain text search.
|
||||||
|
if (_similarMatch != null) {
|
||||||
|
setState(() => _similarMatch = null);
|
||||||
|
}
|
||||||
|
|
||||||
if (trimmed.length >= _minLiveSearchChars) {
|
if (trimmed.length >= _minLiveSearchChars) {
|
||||||
// TextField updates via the controller; only rebuild the list after debounce.
|
// TextField updates via the controller; only rebuild the list after debounce.
|
||||||
_searchDebounceTimer = Timer(_searchDebounce, () {
|
_searchDebounceTimer = Timer(_searchDebounce, () {
|
||||||
@@ -165,13 +192,19 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
|
|
||||||
void _onSearchSubmitted(String value) {
|
void _onSearchSubmitted(String value) {
|
||||||
_searchDebounceTimer?.cancel();
|
_searchDebounceTimer?.cancel();
|
||||||
setState(() => _activeQuery = value.trim());
|
setState(() {
|
||||||
|
_similarMatch = null;
|
||||||
|
_activeQuery = value.trim();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSearch() {
|
void _clearSearch() {
|
||||||
_searchDebounceTimer?.cancel();
|
_searchDebounceTimer?.cancel();
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() => _activeQuery = '');
|
setState(() {
|
||||||
|
_activeQuery = '';
|
||||||
|
_similarMatch = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickFilters(List<TxLabel> available) async {
|
Future<void> _pickFilters(List<TxLabel> available) async {
|
||||||
@@ -183,6 +216,7 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
currentMonth: _month,
|
currentMonth: _month,
|
||||||
initial: TransactionFilterSelection(
|
initial: TransactionFilterSelection(
|
||||||
labelIds: Set<String>.of(_labelFilters),
|
labelIds: Set<String>.of(_labelFilters),
|
||||||
|
labelMatch: _labelMatch,
|
||||||
flow: _flowFilter,
|
flow: _flowFilter,
|
||||||
amountRange: _amountRange,
|
amountRange: _amountRange,
|
||||||
query: _activeQuery,
|
query: _activeQuery,
|
||||||
@@ -195,6 +229,7 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
_labelFilters
|
_labelFilters
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(result.labelIds);
|
..addAll(result.labelIds);
|
||||||
|
_labelMatch = result.labelMatch;
|
||||||
_flowFilter = result.flow;
|
_flowFilter = result.flow;
|
||||||
_amountRange = result.amountRange;
|
_amountRange = result.amountRange;
|
||||||
_searchInMonthOnly = result.monthOnly;
|
_searchInMonthOnly = result.monthOnly;
|
||||||
@@ -240,27 +275,6 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveAsHomeWidget() async {
|
|
||||||
final preferences = context.read<PreferencesController>();
|
|
||||||
final created = await composeHomeFilterWidget(
|
|
||||||
context,
|
|
||||||
initialFilter: TransactionFilterSelection(
|
|
||||||
labelIds: Set<String>.of(_labelFilters),
|
|
||||||
flow: _flowFilter,
|
|
||||||
amountRange: _amountRange,
|
|
||||||
query: _activeQuery,
|
|
||||||
monthOnly: _searchInMonthOnly,
|
|
||||||
),
|
|
||||||
skipFilterDialog: true,
|
|
||||||
);
|
|
||||||
if (created == null) return;
|
|
||||||
await preferences.addHomeFilterWidget(created);
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Added “${created.name}” to Home')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bank = context.watch<BankController>();
|
final bank = context.watch<BankController>();
|
||||||
@@ -269,11 +283,6 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Transactions'),
|
title: const Text('Transactions'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
|
||||||
tooltip: 'Save as Home widget',
|
|
||||||
onPressed: _saveAsHomeWidget,
|
|
||||||
icon: const Icon(Icons.dashboard_customize_outlined),
|
|
||||||
),
|
|
||||||
...bankAppBarActions(
|
...bankAppBarActions(
|
||||||
context: context,
|
context: context,
|
||||||
onRefresh: bank.refresh,
|
onRefresh: bank.refresh,
|
||||||
@@ -386,6 +395,7 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
|||||||
searchPool,
|
searchPool,
|
||||||
_activeQuery,
|
_activeQuery,
|
||||||
labelIds: _labelFilters,
|
labelIds: _labelFilters,
|
||||||
|
labelMatch: _labelMatch,
|
||||||
))
|
))
|
||||||
if (_matchesExtraFilters(tx, metaController)) tx,
|
if (_matchesExtraFilters(tx, metaController)) tx,
|
||||||
];
|
];
|
||||||
|
|||||||