10 Commits
Author SHA1 Message Date
davmar b62164dde5 spinner feedback to ui 2 2026-09-17 00:50:15 +02:00
davmar 5cd9a88682 spinner feedback to ui 2026-09-17 00:50:07 +02:00
davmar e1881acc18 fix pending transaction 2026-09-16 23:59:03 +02:00
davmar f4b914a2ad ios added 2026-09-11 13:27:24 +02:00
davmar 34ef156538 fix tap on chart 2 2026-09-11 13:20:48 +02:00
davmar cfb551328b fix tap on chart 2026-09-11 12:45:50 +02:00
davmar 40366f0708 some issues fixed 2026-09-11 12:31:07 +02:00
davmar 2e40786bcd attempt to fix test txs 2.1 2026-09-11 00:11:00 +02:00
davmar 38bcf01ae3 attempt to fix test txs 2 2026-09-11 00:10:15 +02:00
davmar 00ab05eefc attempt to fix test txs 2026-09-10 23:59:00 +02:00
104 changed files with 4478 additions and 615 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ migration:
- platform: root
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
- platform: android
- platform: ios
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
+1
View File
@@ -13,6 +13,7 @@ analyzer:
exclude:
- build/**
- android/**
- ios/**
linter:
# The lint rules applied to this project can be customized in the
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+662
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<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>
+7
View File
@@ -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
}
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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>
+29
View File
@@ -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>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>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>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+647
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<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>
+7
View File
@@ -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>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>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"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+17
View File
@@ -113,6 +113,23 @@ class BankTransaction {
/// Exact match for copying notes (same description + 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,
+33 -5
View File
@@ -94,6 +94,8 @@ class HomeFilterWidget {
this.showAmountIn = true,
this.showAmountOut = true,
this.showCount = true,
this.updatedAt = 0,
this.deleted = false,
});
final String id;
@@ -129,6 +131,12 @@ class HomeFilterWidget {
final bool showAmountOut;
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);
TransactionFilterSelection get filter => TransactionFilterSelection(
@@ -161,6 +169,9 @@ class HomeFilterWidget {
bool? showAmountIn,
bool? showAmountOut,
bool? showCount,
int? updatedAt,
bool? deleted,
bool touchUpdatedAt = false,
}) {
return HomeFilterWidget(
id: id ?? this.id,
@@ -183,6 +194,10 @@ class HomeFilterWidget {
showAmountIn: showAmountIn ?? this.showAmountIn,
showAmountOut: showAmountOut ?? this.showAmountOut,
showCount: showCount ?? this.showCount,
updatedAt: touchUpdatedAt
? DateTime.now().millisecondsSinceEpoch
: (updatedAt ?? this.updatedAt),
deleted: deleted ?? this.deleted,
);
}
@@ -208,6 +223,7 @@ class HomeFilterWidget {
showAmountIn: showIn,
showAmountOut: showOut,
showCount: true,
updatedAt: DateTime.now().millisecondsSinceEpoch,
);
}
@@ -236,6 +252,8 @@ class HomeFilterWidget {
'showAmountIn': showAmountIn,
'showAmountOut': showAmountOut,
'showCount': showCount,
'updatedAt': updatedAt,
'deleted': deleted,
};
factory HomeFilterWidget.fromJson(Map<String, dynamic> json) {
@@ -302,21 +320,31 @@ class HomeFilterWidget {
showAmountIn: json['showAmountIn'] as bool? ?? true,
showAmountOut: json['showAmountOut'] 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> local,
List<HomeFilterWidget> remote,
) {
final byId = <String, HomeFilterWidget>{
for (final w in remote) w.id: w,
};
for (final w in local) {
final byId = <String, HomeFilterWidget>{};
void put(HomeFilterWidget w) {
final existing = byId[w.id];
if (existing == null || w.updatedAt >= existing.updatedAt) {
byId[w.id] = w;
}
}
for (final w in remote) {
put(w);
}
for (final w in local) {
put(w);
}
final merged = byId.values.toList()
..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
return merged;
+48
View File
@@ -16,6 +16,8 @@ class OverviewFavourite {
this.flow = TxFlowFilter.expense,
this.barStyle = LabelChartBarStyle.parallel,
this.invertExpensePerspective = false,
this.updatedAt = 0,
this.deleted = false,
});
final String id;
@@ -34,6 +36,12 @@ class OverviewFavourite {
final LabelChartBarStyle barStyle;
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({
String? id,
String? name,
@@ -44,6 +52,9 @@ class OverviewFavourite {
TxFlowFilter? flow,
LabelChartBarStyle? barStyle,
bool? invertExpensePerspective,
int? updatedAt,
bool? deleted,
bool touchUpdatedAt = false,
}) {
return OverviewFavourite(
id: id ?? this.id,
@@ -56,6 +67,10 @@ class OverviewFavourite {
barStyle: barStyle ?? this.barStyle,
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,
'barStyle': barStyle.name,
'invertExpensePerspective': invertExpensePerspective,
'updatedAt': updatedAt,
'deleted': deleted,
};
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(
id: json['id'] as String? ?? 'fav_${DateTime.now().millisecondsSinceEpoch}',
name: (json['name'] as String?)?.trim().isNotEmpty == true
@@ -100,6 +124,8 @@ class OverviewFavourite {
),
invertExpensePerspective:
json['invertExpensePerspective'] as bool? ?? false,
updatedAt: updatedAt,
deleted: json['deleted'] as bool? ?? false,
);
}
}
@@ -107,3 +133,25 @@ class OverviewFavourite {
enum OverviewMetricMode { flow, labels }
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);
}
+131 -23
View File
@@ -1,6 +1,8 @@
/// [ChangeNotifier] for bank link status, balance, and transactions.
library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/bank.dart';
@@ -36,6 +38,18 @@ class BankController extends ChangeNotifier {
_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();
}
@@ -55,29 +69,90 @@ class BankController extends ChangeNotifier {
/// cannot leave the device behind the cloud (or wipe cloud-only history).
VoidCallback? onAfterSuccessfulLoad;
/// Fired when local admin txs are removed/replaced and need a remote delete.
VoidCallback? onPendingRemoteDeletesChanged;
ConnectStatus _status = ConnectStatus.disconnected;
BankBalance? _balance;
List<BankTransaction> _remoteTransactions = const [];
List<BankTransaction> _importedTransactions = const [];
List<String> _pendingRemoteDeletes = const [];
List<BankTransaction> _pendingRemoteDeletes = const [];
/// Local tx_ids removed/replaced that should be deleted on the next PB push.
List<String> takePendingRemoteDeletes() {
final out = List<String>.of(_pendingRemoteDeletes);
_pendingRemoteDeletes = const [];
return out;
/// 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<String> txIds) {
for (final id in txIds) {
_queueRemoteDelete(id);
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();
}
}
void _queueRemoteDelete(String txId) {
if (txId.isEmpty) return;
if (_pendingRemoteDeletes.contains(txId)) return;
_pendingRemoteDeletes = [..._pendingRemoteDeletes, txId];
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.
@@ -300,7 +375,7 @@ class BankController extends ChangeNotifier {
if (tx.id != previous.id) tx,
];
if (previous.id != updated.id) {
_queueRemoteDelete(previous.id);
_queueRemoteDelete(previous);
}
_importedTransactions = mergeTransactions(_importedTransactions, [updated]);
_invalidateMerged();
@@ -317,13 +392,13 @@ class BankController extends ChangeNotifier {
}
_importedTransactions = [
for (final row in _importedTransactions)
if (row.id != tx.id) row,
if (!row.matchesForDelete(tx)) row,
];
_remoteTransactions = [
for (final row in _remoteTransactions)
if (row.id != tx.id) row,
if (!row.matchesForDelete(tx)) row,
];
_queueRemoteDelete(tx.id);
_queueRemoteDelete(tx);
_invalidateMerged();
await _persistImported();
await _persistCache();
@@ -337,17 +412,38 @@ class BankController extends ChangeNotifier {
List<BankTransaction> fromCloud, {
bool notify = true,
}) async {
if (fromCloud.isEmpty) return;
if (fromCloud.isEmpty && _pendingRemoteDeletes.isEmpty) return;
final manuals = _collectManualIds(fromCloud.map(_upgradeLegacyManual));
_remoteTransactions = mergeTransactions(
_remoteTransactions,
[for (final tx in fromCloud) _upgradeLegacyManual(tx)],
// 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(
_remoteTransactions,
_importedTransactions,
);
if (_pendingRemoteDeletes.isNotEmpty) {
_importedTransactions = [
for (final tx in _importedTransactions)
if (!_isTombstoned(tx)) tx,
];
}
_reapplyManualFlags(manuals);
_invalidateMerged();
@@ -428,8 +524,10 @@ class BankController extends ChangeNotifier {
final apiTxs = results[1] as List<BankTransaction>;
// Bank API is ~89 days. Prefer API rows for overlaps, but keep any
// cloud/CSV history that the API window does not include.
// Pending rows are not kept from cache — they must still be on the feed.
final manuals = _collectManualIds();
_remoteTransactions = mergeTransactions(apiTxs, _remoteTransactions);
_remoteTransactions =
mergeApiTransactionsWithCache(apiTxs, _remoteTransactions);
if (!_status.linked) {
_status = ConnectStatus(
status: 'linked',
@@ -444,6 +542,16 @@ class BankController extends ChangeNotifier {
_remoteTransactions,
_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();
await _persistImported();
+66 -30
View File
@@ -42,26 +42,40 @@ class PreferencesController extends ChangeNotifier {
);
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<OverviewFavourite> get displayedOverviewFavourites => [
for (final fav in _overviewFavourites)
if (fav.display) fav,
if (!fav.deleted && fav.display) fav,
];
bool get hasDisplayedOverviewFavourites =>
_overviewFavourites.any((f) => f.display);
_overviewFavourites.any((f) => !f.deleted && f.display);
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<HomeFilterWidget> get displayedHomeFilterWidgets => [
for (final w in _homeFilterWidgets)
if (w.display) w,
if (!w.deleted && w.display) w,
];
bool get hasDisplayedHomeFilterWidgets =>
_homeFilterWidgets.any((w) => w.display);
_homeFilterWidgets.any((w) => !w.deleted && w.display);
HomeBuiltInSections get homeBuiltInSections => _homeBuiltInSections;
@@ -118,7 +132,10 @@ class PreferencesController extends ChangeNotifier {
}
Future<void> addOverviewFavourite(OverviewFavourite favourite) async {
_overviewFavourites = [..._overviewFavourites, favourite];
final stamped = favourite.updatedAt == 0
? favourite.copyWith(touchUpdatedAt: true)
: favourite;
_overviewFavourites = [..._overviewFavourites, stamped];
notifyListeners();
await _persistFavourites();
}
@@ -127,18 +144,19 @@ class PreferencesController extends ChangeNotifier {
final index = _overviewFavourites.indexWhere((f) => f.id == favourite.id);
if (index < 0) return;
final next = [..._overviewFavourites];
next[index] = favourite;
next[index] = favourite.copyWith(touchUpdatedAt: true);
_overviewFavourites = next;
notifyListeners();
await _persistFavourites();
}
Future<void> removeOverviewFavourite(String id) async {
final next = [
for (final fav in _overviewFavourites)
if (fav.id != id) fav,
];
if (next.length == _overviewFavourites.length) return;
final index = _overviewFavourites.indexWhere((f) => f.id == id);
if (index < 0) return;
final current = _overviewFavourites[index];
if (current.deleted) return;
final next = [..._overviewFavourites];
next[index] = current.copyWith(deleted: true, touchUpdatedAt: true);
_overviewFavourites = next;
notifyListeners();
await _persistFavourites();
@@ -148,7 +166,7 @@ class PreferencesController extends ChangeNotifier {
final index = _overviewFavourites.indexWhere((f) => f.id == id);
if (index < 0) return;
final current = _overviewFavourites[index];
if (current.display == display) return;
if (current.deleted || current.display == display) return;
await updateOverviewFavourite(current.copyWith(display: display));
}
@@ -164,16 +182,20 @@ class PreferencesController extends ChangeNotifier {
}
Future<void> addHomeFilterWidget(HomeFilterWidget widget) async {
final order = _homeFilterWidgets.isEmpty
final order = _homeFilterWidgets.where((w) => !w.deleted).isEmpty
? 0
: _homeFilterWidgets
.where((w) => !w.deleted)
.map((w) => w.sortOrder)
.reduce((a, b) => a > b ? a : b) +
1;
_homeFilterWidgets = [
..._homeFilterWidgets,
widget.copyWith(sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder),
];
final stamped = (widget.updatedAt == 0
? widget.copyWith(touchUpdatedAt: true)
: widget)
.copyWith(
sortOrder: widget.sortOrder == 0 ? order : widget.sortOrder,
);
_homeFilterWidgets = [..._homeFilterWidgets, stamped];
notifyListeners();
await _persistHomeWidgets();
}
@@ -182,7 +204,7 @@ class PreferencesController extends ChangeNotifier {
final index = _homeFilterWidgets.indexWhere((w) => w.id == widget.id);
if (index < 0) return;
final next = [..._homeFilterWidgets];
next[index] = widget;
next[index] = widget.copyWith(touchUpdatedAt: true);
next.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
_homeFilterWidgets = next;
notifyListeners();
@@ -190,11 +212,12 @@ class PreferencesController extends ChangeNotifier {
}
Future<void> removeHomeFilterWidget(String id) async {
final next = [
for (final w in _homeFilterWidgets)
if (w.id != id) w,
];
if (next.length == _homeFilterWidgets.length) return;
final index = _homeFilterWidgets.indexWhere((w) => w.id == id);
if (index < 0) return;
final current = _homeFilterWidgets[index];
if (current.deleted) return;
final next = [..._homeFilterWidgets];
next[index] = current.copyWith(deleted: true, touchUpdatedAt: true);
_homeFilterWidgets = next;
notifyListeners();
await _persistHomeWidgets();
@@ -204,19 +227,32 @@ class PreferencesController extends ChangeNotifier {
final index = _homeFilterWidgets.indexWhere((w) => w.id == id);
if (index < 0) return;
final current = _homeFilterWidgets[index];
if (current.display == display) return;
if (current.deleted || current.display == display) return;
await updateHomeFilterWidget(current.copyWith(display: display));
}
Future<void> reorderHomeFilterWidgets(int oldIndex, int newIndex) async {
if (oldIndex == newIndex) return;
final next = [..._homeFilterWidgets];
final item = next.removeAt(oldIndex);
final target = newIndex.clamp(0, next.length);
next.insert(target, item);
_homeFilterWidgets = [
for (var i = 0; i < next.length; i++) next[i].copyWith(sortOrder: i),
final visible = [
for (final w in _homeFilterWidgets)
if (!w.deleted) w,
];
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();
await _persistHomeWidgets();
}
+101 -21
View File
@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/app_cloud_prefs.dart';
import '../models/bank.dart';
import '../models/home_filter_widget.dart';
import '../models/overview_favourite.dart';
import '../services/pb_sync_service.dart';
@@ -46,6 +47,7 @@ class SyncController extends ChangeNotifier {
_prefs.onAppPrefsPersisted = _scheduleAppPrefsPush;
_auth.addListener(_onAuthChanged);
_bank.onAfterSuccessfulLoad = _onBankDataLoaded;
_bank.onPendingRemoteDeletesChanged = _scheduleBankTxPush;
if (_auth.isLoggedIn) {
unawaited(syncNow());
}
@@ -64,6 +66,7 @@ class SyncController extends ChangeNotifier {
bool _pushFavouritesPending = false;
bool _pushHomeWidgetsPending = false;
bool _pushAppPrefsPending = false;
bool _pushBankPending = false;
Timer? _pushDebounce;
Timer? _progressHold;
Timer? _uiNotifyThrottle;
@@ -198,6 +201,7 @@ class SyncController extends ChangeNotifier {
_pushFavouritesPending = false;
_pushHomeWidgetsPending = false;
_pushAppPrefsPending = false;
_pushBankPending = false;
_lastError = null;
_statusDetail = '';
_showProgressRing = false;
@@ -265,6 +269,47 @@ class SyncController extends ChangeNotifier {
_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() {
if (!_auth.isLoggedIn) return;
if (_syncing) {
@@ -297,6 +342,9 @@ class SyncController extends ChangeNotifier {
try {
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(
onProgress: (p) {
_setProgress(p * 0.35);
@@ -324,7 +372,7 @@ class SyncController extends ChangeNotifier {
);
}
final mergedFavourites = mergeOverviewFavourites(
_prefs.overviewFavourites,
_prefs.overviewFavouritesForSync,
remoteFavourites,
);
List<HomeFilterWidget> remoteHomeWidgets = const [];
@@ -338,7 +386,7 @@ class SyncController extends ChangeNotifier {
);
}
final mergedHomeWidgets = mergeHomeFilterWidgets(
_prefs.homeFilterWidgets,
_prefs.homeFilterWidgetsForSync,
remoteHomeWidgets,
);
AppCloudPrefs? remoteAppPrefs;
@@ -408,17 +456,14 @@ class SyncController extends ChangeNotifier {
} catch (error) {
debugPrint('SyncController: app_prefs push skipped ($error)');
}
final pendingDeletes = _bank.takePendingRemoteDeletes();
if (pendingDeletes.isNotEmpty) {
try {
await service.deleteTransactionsByTxIds(pendingDeletes);
} catch (error) {
debugPrint('SyncController: tx deletes skipped ($error)');
_bank.requeueRemoteDeletes(pendingDeletes);
}
}
// 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(
_bank.transactions,
deleteTombs: tombs,
progressStart: 0.82,
progressEnd: 1.0,
onProgress: _setProgress,
@@ -463,11 +508,17 @@ class SyncController extends ChangeNotifier {
final pushFavourites = _pushFavouritesPending;
final pushHomeWidgets = _pushHomeWidgetsPending;
final pushAppPrefs = _pushAppPrefsPending;
final pushBank = _pushBankPending || _bank.pendingRemoteDeletes.isNotEmpty;
_pushMetaPending = false;
_pushFavouritesPending = false;
_pushHomeWidgetsPending = false;
_pushAppPrefsPending = false;
if (!pushMeta && !pushFavourites && !pushHomeWidgets && !pushAppPrefs) {
_pushBankPending = false;
if (!pushMeta &&
!pushFavourites &&
!pushHomeWidgets &&
!pushAppPrefs &&
!pushBank) {
return;
}
@@ -480,13 +531,15 @@ class SyncController extends ChangeNotifier {
pushFavourites,
pushHomeWidgets,
pushAppPrefs,
pushBank,
)) {
(false, false, false, true) => 'App prefs',
(false, false, true, false) => 'Home widgets',
(false, false, true, true) => 'Home layout',
(false, true, false, false) => 'Favourites',
(false, true, true, _) => 'Home layout',
(true, false, false, false) => 'Annotations',
(false, false, false, false, true) => 'Bank txs',
(false, false, false, true, false) => 'App prefs',
(false, false, true, false, false) => 'Home widgets',
(false, false, true, true, _) => 'Home layout',
(false, true, false, false, false) => 'Favourites',
(false, true, true, _, _) => 'Home layout',
(true, false, false, false, false) => 'Annotations',
_ => 'Changes',
};
_statusDetail = 'Pushing $label…';
@@ -500,7 +553,8 @@ class SyncController extends ChangeNotifier {
final total = (pushMeta ? 1 : 0) +
(pushFavourites ? 1 : 0) +
(pushHomeWidgets ? 1 : 0) +
(pushAppPrefs ? 1 : 0);
(pushAppPrefs ? 1 : 0) +
(pushBank ? 1 : 0);
void bump() {
done++;
_setProgress(done / total);
@@ -522,7 +576,7 @@ class SyncController extends ChangeNotifier {
_setStatus('Pushing favourites…');
try {
await service.pushOverviewFavourites(
_prefs.overviewFavourites,
_prefs.overviewFavouritesForSync,
onStatus: _setStatus,
);
parts.add('Favourites updated');
@@ -530,6 +584,7 @@ class SyncController extends ChangeNotifier {
debugPrint(
'SyncController: overview_favourites push skipped ($error)',
);
_pushFavouritesPending = true;
}
bump();
}
@@ -537,7 +592,7 @@ class SyncController extends ChangeNotifier {
_setStatus('Pushing home widgets…');
try {
await service.pushHomeFilterWidgets(
_prefs.homeFilterWidgets,
_prefs.homeFilterWidgetsForSync,
onStatus: _setStatus,
);
parts.add('Home widgets updated');
@@ -545,6 +600,7 @@ class SyncController extends ChangeNotifier {
debugPrint(
'SyncController: home_filter_widgets push skipped ($error)',
);
_pushHomeWidgetsPending = true;
}
bump();
}
@@ -564,6 +620,29 @@ class SyncController extends ChangeNotifier {
}
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);
_lastSyncedAt = DateTime.now();
@@ -597,6 +676,7 @@ class SyncController extends ChangeNotifier {
_uiNotifyThrottle?.cancel();
_auth.removeListener(_onAuthChanged);
_bank.onAfterSuccessfulLoad = null;
_bank.onPendingRemoteDeletesChanged = null;
_meta.onPersisted = null;
_prefs.onFavouritesPersisted = null;
_prefs.onHomeWidgetsPersisted = null;
+136 -9
View File
@@ -10,8 +10,10 @@ import '../providers/preferences_provider.dart';
import '../providers/tx_meta_provider.dart';
import '../utils/app_theme.dart';
import '../utils/connect_bank.dart';
import '../utils/deferred_tab_work.dart';
import '../utils/helpers.dart';
import '../utils/navigation.dart';
import '../utils/tx_filter_metrics.dart';
import '../widgets/empty_state.dart';
import '../widgets/home_filter_widget_card.dart';
import '../widgets/home_widgets_sheet.dart';
@@ -22,6 +24,18 @@ import '../widgets/skip_build_when_covered.dart';
import 'transaction_detail_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 {
const HomeScreen({super.key});
@@ -33,16 +47,93 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
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
Widget build(BuildContext context) {
// Inactive IndexedStack tabs must not watch providers — otherwise every
// 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();
}
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(
appBar: AppBar(
@@ -54,7 +145,9 @@ class _HomeScreenState extends State<HomeScreen> {
onPressed: () =>
setState(() => _editingWidgets = !_editingWidgets),
icon: Icon(
_editingWidgets ? Icons.check : Icons.dashboard_customize_outlined,
_editingWidgets
? Icons.check
: Icons.dashboard_customize_outlined,
),
),
...bankAppBarActions(
@@ -67,9 +160,18 @@ class _HomeScreenState extends State<HomeScreen> {
body: RefreshIndicator(
onRefresh: bank.refresh,
child: ResponsiveBody(
child: _HomeBody(
child: showSpinner
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: const [
SizedBox(height: 120),
LoadingView(message: 'Loading home…'),
],
)
: _HomeBody(
bank: bank,
editingWidgets: _editingWidgets,
snapshot: _work.value,
),
),
),
@@ -81,10 +183,12 @@ class _HomeBody extends StatelessWidget {
const _HomeBody({
required this.bank,
required this.editingWidgets,
required this.snapshot,
});
final BankController bank;
final bool editingWidgets;
final _HomeSnapshot? snapshot;
void _openTransactions(BuildContext context) {
Navigator.of(context).push(
@@ -141,7 +245,7 @@ class _HomeBody extends StatelessWidget {
final colors = theme.colorScheme;
final meta = context.watch<TxMetaController>();
final recent = bank.transactions.take(5).toList(growable: false);
final categorized =
final categorized = snapshot?.categorized ??
meta.labeledTransactionCount(bank.transactions);
final widgets = editingWidgets
? prefs.homeFilterWidgets
@@ -319,6 +423,9 @@ class _HomeBody extends StatelessWidget {
child: HomeFilterWidgetCard(
widgetConfig: widgetConfig,
editing: true,
precomputedStats: snapshot?.statsById[widgetConfig.id],
precomputedBudget:
snapshot?.budgetById[widgetConfig.id],
onToggleDisplay: (display) =>
prefs.setHomeFilterWidgetDisplay(
widgetConfig.id,
@@ -347,7 +454,11 @@ class _HomeBody extends StatelessWidget {
},
)
else
..._packedHomeWidgetRows(widgets),
..._packedHomeWidgetRows(
widgets,
statsById: snapshot?.statsById,
budgetById: snapshot?.budgetById,
),
],
if (_showBuiltIn(
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>[];
var i = 0;
while (i < widgets.length) {
@@ -571,11 +686,19 @@ List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: HomeFilterWidgetCard(widgetConfig: a),
child: HomeFilterWidgetCard(
widgetConfig: a,
precomputedStats: statsById?[a.id],
precomputedBudget: budgetById?[a.id],
),
),
const SizedBox(width: 10),
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),
child: _HomeWidgetSlot(
halfWidth: a.halfWidth,
child: HomeFilterWidgetCard(widgetConfig: a),
child: HomeFilterWidgetCard(
widgetConfig: a,
precomputedStats: statsById?[a.id],
precomputedBudget: budgetById?[a.id],
),
),
),
);
+103 -16
View File
@@ -1,6 +1,8 @@
/// Look ahead: projected balance from recurring templates through a date.
library;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -16,6 +18,7 @@ import '../utils/look_ahead.dart';
import '../utils/navigation.dart';
import '../utils/tx_calendar.dart';
import '../widgets/empty_state.dart';
import '../widgets/loading_view.dart';
import '../widgets/recurring_template_sheet.dart';
import '../widgets/responsive_body.dart';
import '../widgets/shell_tab_scope.dart';
@@ -23,6 +26,13 @@ import '../widgets/skip_build_when_covered.dart';
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 {
const LookAheadScreen({super.key});
@@ -35,8 +45,9 @@ class LookAheadScreen extends StatefulWidget {
class _LookAheadScreenState extends State<LookAheadScreen> {
var _period = _PeriodMode.monthly;
late DateTime _cursor;
List<RecurringItem>? _cachedItems;
int? _itemsCacheKey;
_ItemsSnapshot? _snapshot;
int? _loadingKey;
int _computeGen = 0;
@override
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
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 SkipBuildWhenCovered(
@@ -165,22 +245,29 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
final currentAmount = balance?.amount ?? 0;
final today = _today;
final through = _through;
final cacheKey = Object.hash(
meta.storeRevision,
identityHashCode(bank.transactions),
);
if (_cachedItems == null || _itemsCacheKey != cacheKey) {
_itemsCacheKey = cacheKey;
_cachedItems = recurringItemsFromTemplates(
templates: meta.recurringTemplates,
transactions: bank.transactions,
isInternalTransfer: (tx) => meta.metaFor(tx).internalTransfer,
labelIdsFor: meta.labelIdsFor,
labelName: (id) => meta.labelById(id)?.name ?? 'label',
final cacheKey = _itemsCacheKey(meta, bank);
if (_snapshot?.key != cacheKey && _loadingKey != cacheKey) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (ShellTabScope.of(context) != LookAheadScreen.tabIndex) return;
_ensureItemsLoaded(cacheKey: cacheKey, meta: meta, bank: bank);
});
}
final snapshot = _snapshot;
final ready = snapshot != null && snapshot.key == cacheKey;
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(
items: items,
from: today,
+339 -103
View File
@@ -12,6 +12,7 @@ import '../providers/bank_provider.dart';
import '../providers/preferences_provider.dart';
import '../providers/tx_meta_provider.dart';
import '../utils/app_theme.dart';
import '../utils/deferred_tab_work.dart';
import '../utils/helpers.dart';
import '../utils/navigation.dart';
import '../utils/tx_calendar.dart';
@@ -28,6 +29,36 @@ import 'transactions_screen.dart';
const _kIncomeColor = AppTheme.accent;
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 {
const OverviewScreen({super.key});
@@ -59,6 +90,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
/// Favourites charts view (vs full overview with history).
bool _showFavouritesView = false;
var _favouritesDefaultApplied = false;
final _work = DeferredTabWork<_OverviewAggSnapshot>();
@override
void initState() {
@@ -247,7 +279,15 @@ class _OverviewScreenState extends State<OverviewScreen> {
@override
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();
}
@@ -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(
appBar: AppBar(
title: Text(_showFavouritesView ? 'Favourites' : 'Overview'),
@@ -288,7 +363,15 @@ class _OverviewScreenState extends State<OverviewScreen> {
body: RefreshIndicator(
onRefresh: bank.refresh,
child: ResponsiveBody(
child: SkipBuildWhenCovered(
child: showSpinner
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: const [
SizedBox(height: 120),
LoadingView(message: 'Loading overview…'),
],
)
: SkipBuildWhenCovered(
builder: (context) => _showFavouritesView
? _buildFavouritesBody(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(
BuildContext context,
BankController bank,
@@ -329,6 +495,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
: 'EUR');
final txs = bank.transactions;
final displayed = prefs.displayedOverviewFavourites;
final agg = _work.value;
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -399,6 +566,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
txs: txs,
meta: meta,
currency: currency,
precomputed: agg?.favourites[displayed[i].id],
),
],
],
@@ -412,12 +580,16 @@ class _OverviewScreenState extends State<OverviewScreen> {
required List<BankTransaction> txs,
required TxMetaController meta,
required String currency,
_FavouriteAgg? precomputed,
}) {
final periods = _chartPeriodsFor(favourite.period, txs);
Map<DateTime, _MonthBucket> monthly = const {};
var labelSpend = const <_LabelSpend>[];
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
final periods = precomputed?.periods ??
_chartPeriodsFor(favourite.period, txs);
Map<DateTime, _MonthBucket> monthly = precomputed?.monthly ?? const {};
var labelSpend = precomputed?.labelSpend ?? const <_LabelSpend>[];
Map<DateTime, Map<String, double>> labelAmountByPeriod =
precomputed?.labelAmountByPeriod ?? const {};
if (precomputed == null) {
if (favourite.metric == OverviewMetricMode.flow) {
monthly = _flowTotals(txs, meta, periods, favourite.period);
} else {
@@ -432,6 +604,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
labelSpend = labels.spend;
labelAmountByPeriod = labels.amountByPeriod;
}
}
final periodTitle = favourite.period == OverviewPeriodMode.monthly
? '${_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
: 'EUR');
final txs = bank.transactions;
final periods = _chartPeriods(txs);
final agg = _work.value;
final periods = agg?.periods ?? _chartPeriods(txs);
final historyPeriods = [...periods.reversed];
// Only aggregate what the active metric needs — toggling was scanning
// all txs 3–4 times and resolving full meta each time.
Map<DateTime, _MonthBucket> monthly = const {};
var labelSpend = const <_LabelSpend>[];
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
// Prefer deferred aggregates; fall back only if snapshot is missing.
Map<DateTime, _MonthBucket> monthly = agg?.monthly ?? const {};
var labelSpend = agg?.labelSpend ?? const <_LabelSpend>[];
Map<DateTime, Map<String, double>> labelAmountByPeriod =
agg?.labelAmountByPeriod ?? const {};
if (agg == null) {
if (_metric == OverviewMetricMode.flow) {
monthly = _flowTotals(txs, meta, periods, _period);
} else {
@@ -531,6 +728,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
labelSpend = labels.spend;
labelAmountByPeriod = labels.amountByPeriod;
}
}
final summaryFlow = _metric == OverviewMetricMode.flow
? _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),
if (_metric == OverviewMetricMode.flow) ...[
@@ -739,23 +947,57 @@ class _OverviewScreenState extends State<OverviewScreen> {
BuildContext context,
DateTime period, {
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 labelIds = labelId != null
final resolvedLabelIds = labelId != null
? {labelId}
: {
: (labelIds ??
{
// Open with the same labels currently shown on the chart.
for (final id in _includedLabelIds) id,
});
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => TransactionsScreen(
initialMonth: initial,
initialLabelIds: resolvedLabelIds,
initialLabelMatch: LabelMatchMode.or,
initialFlow: flow,
initialSearchInMonthOnly: !yearly && resolvedLabelIds.isNotEmpty,
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(
MaterialPageRoute<void>(
builder: (_) => TransactionsScreen(
initialMonth: initial,
initialLabelIds: labelIds,
// Monthly charts show a year — open that whole year with the chart filter.
initialMonth: yearly ? null : DateTime(_cursor.year, 12),
initialYear: yearly ? null : _cursor.year,
initialLabelIds: resolvedLabelIds,
initialLabelMatch: LabelMatchMode.or,
initialSearchInMonthOnly: !yearly && labelIds.isNotEmpty,
initialYear: yearly ? period.year : null,
initialFlow: flow,
initialSearchInMonthOnly: false,
),
),
);
@@ -1159,6 +1401,7 @@ class _ChartCard extends StatelessWidget {
required this.availableLabelCount,
required this.onLabelTap,
required this.onMonthTap,
required this.onChartTap,
this.onPeriodModeChanged,
this.onEdit,
});
@@ -1182,7 +1425,12 @@ class _ChartCard extends StatelessWidget {
final int? includedLabelCount;
final int availableLabelCount;
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 VoidCallback? onEdit;
@@ -1277,7 +1525,7 @@ class _ChartCard extends StatelessWidget {
runSpacing: 2,
alignment: WrapAlignment.end,
children: [
for (final row in labelSpend.take(compact ? 3 : 5))
for (final row in labelSpend.take(6))
GestureDetector(
onTap: () => onLabelTap(row.label),
child: _LegendDot(
@@ -1286,7 +1534,7 @@ class _ChartCard extends StatelessWidget {
compact: compact,
),
),
if (labelSpend.length > (compact ? 3 : 5))
if (labelSpend.length > 6)
_LegendDot(
color: colors.outline,
label: 'Other',
@@ -1359,13 +1607,18 @@ class _ChartCard extends StatelessWidget {
final hasLegend = metric == OverviewMetricMode.flow ||
labelSpend.isNotEmpty;
final radius = BorderRadius.circular(compact ? 16 : 20);
return Card(
color: colors.surfaceContainer,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(compact ? 16 : 20),
),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: radius),
// Whole card opens the chart filter; nested buttons/legend keep their own taps.
child: InkWell(
onTap: onChartTap,
borderRadius: radius,
child: Padding(
padding: EdgeInsets.fromLTRB(
compact ? 12 : 14,
@@ -1529,6 +1782,8 @@ class _ChartCard extends StatelessWidget {
monthly: monthly,
currency: currency,
period: period,
onMonthTap: onMonthTap,
onChartTap: onChartTap,
)
: _LabelMonthBars(
months: months,
@@ -1539,11 +1794,13 @@ class _ChartCard extends StatelessWidget {
barStyle: labelBarStyle,
invertChart: invertChart,
onMonthTap: onMonthTap,
onChartTap: onChartTap,
),
),
],
),
),
),
);
}
}
@@ -1554,12 +1811,20 @@ class _FlowBars extends StatelessWidget {
required this.monthly,
required this.currency,
required this.period,
required this.onMonthTap,
required this.onChartTap,
});
final List<DateTime> months;
final Map<DateTime, _MonthBucket> monthly;
final String currency;
final OverviewPeriodMode period;
final void Function(
DateTime month, {
String? labelId,
TxFlowFilter? flow,
}) onMonthTap;
final VoidCallback onChartTap;
@override
Widget build(BuildContext context) {
@@ -1578,7 +1843,10 @@ class _FlowBars extends StatelessWidget {
});
if (empty) {
return Center(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text(
period == OverviewPeriodMode.yearly
? 'No income or expenses in these years'
@@ -1586,31 +1854,24 @@ class _FlowBars extends StatelessWidget {
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
);
}
return BarChart(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: IgnorePointer(
// fl_chart otherwise owns the gesture arena and empty taps go nowhere.
// Tap opens this chart's filters; legend / history still open columns.
child: BarChart(
BarChartData(
maxY: safeMax,
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final month = months[group.x.toInt()];
final bucket = monthly[_periodKey(month, period)]!;
final isIncome = rodIndex == 1;
final value = isIncome ? bucket.income : bucket.expense;
return BarTooltipItem(
'${_periodAxisLabel(month, period)}\n'
'${isIncome ? 'Income' : 'Expense'}\n'
'${formatMoney(value, currency)}',
TextStyle(color: colors.onInverseSurface),
);
},
),
),
barTouchData: const BarTouchData(enabled: false),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
@@ -1693,6 +1954,8 @@ class _FlowBars extends StatelessWidget {
],
),
duration: Duration.zero,
),
),
);
}
}
@@ -1707,9 +1970,10 @@ class _LabelMonthBars extends StatelessWidget {
required this.barStyle,
required this.invertChart,
required this.onMonthTap,
required this.onChartTap,
});
static const _maxSeriesLabels = 5;
static const _maxSeriesLabels = 6;
final List<DateTime> months;
final List<_LabelSpend> labelSpend;
@@ -1718,7 +1982,12 @@ class _LabelMonthBars extends StatelessWidget {
final OverviewPeriodMode period;
final LabelChartBarStyle barStyle;
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;
@@ -1727,12 +1996,16 @@ class _LabelMonthBars extends StatelessWidget {
final theme = Theme.of(context);
final colors = theme.colorScheme;
if (labelSpend.isEmpty) {
return Center(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text(
'No labeled activity in this period.\nTag transactions to see them here.',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
);
}
@@ -1789,12 +2062,16 @@ class _LabelMonthBars extends StatelessWidget {
}
}
if (maxY == 0 && minY == 0) {
return Center(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text(
'No labeled activity in this period.\nTag transactions to see them here.',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
);
}
final span = (maxY - minY).abs();
@@ -1805,12 +2082,12 @@ class _LabelMonthBars extends StatelessWidget {
final rodWidth = stacked
? (period == OverviewPeriodMode.yearly ? 18.0 : 14.0)
: period == OverviewPeriodMode.yearly
? (seriesCount <= 3 ? 12.0 : 8.0)
? (seriesCount <= 3 ? 12.0 : seriesCount <= 6 ? 8.0 : 6.0)
: (seriesCount <= 3
? 7.0
: seriesCount <= 5
? 5.0
: 4.0);
: seriesCount <= 6
? 4.5
: 3.5);
List<BarChartRodData> rodsFor(DateTime month) {
final bucket =
@@ -1888,59 +2165,16 @@ class _LabelMonthBars extends StatelessWidget {
final axisInterval = ((safeMax - safeMin) / 3).abs();
final interval = axisInterval <= 0 ? 1.0 : axisInterval;
return BarChart(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: IgnorePointer(
child: BarChart(
BarChartData(
minY: safeMin,
maxY: safeMax,
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
barTouchData: BarTouchData(
touchCallback: (event, response) {
if (event is! FlTapUpEvent) return;
final index = response?.spot?.touchedBarGroupIndex;
if (index == null || index < 0 || index >= months.length) return;
onMonthTap(months[index]);
},
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),
);
},
),
),
barTouchData: const BarTouchData(enabled: false),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
@@ -2013,6 +2247,8 @@ class _LabelMonthBars extends StatelessWidget {
],
),
duration: Duration.zero,
),
),
);
}
}
+76
View File
@@ -27,7 +27,31 @@ bool isSameTransaction(BankTransaction a, BankTransaction b) {
if (a.date != b.date) return false;
if (a.currency.toUpperCase() != b.currency.toUpperCase()) return false;
if (_amountCents(a.amount) != _amountCents(b.amount)) return false;
return _descriptionsLikelyMatch(a, b);
}
/// True when a cached/API [pending] row is the unsettled form of [booked].
///
/// Banks often change booking date (and sometimes wording) when a payment
/// settles, so this allows a small date window unlike [isSameTransaction].
bool isPendingSettledBy(
BankTransaction pending,
BankTransaction booked, {
int maxDayGap = 10,
}) {
if (!pending.pending || booked.pending) return false;
if (pending.isManual || booked.isManual) return false;
if (pending.currency.toUpperCase() != booked.currency.toUpperCase()) {
return false;
}
if (_amountCents(pending.amount) != _amountCents(booked.amount)) return false;
if (!_descriptionsLikelyMatch(pending, booked)) return false;
final gap = _dateDiffDays(pending.date, booked.date);
if (gap == null) return false;
return gap.abs() <= maxDayGap;
}
bool _descriptionsLikelyMatch(BankTransaction a, BankTransaction b) {
if (a.descriptionKey == b.descriptionKey) return true;
final ma = a.merchantKey;
@@ -47,10 +71,62 @@ bool isSameTransaction(BankTransaction a, BankTransaction b) {
int _amountCents(double amount) => (amount * 100).round();
int? _dateDiffDays(String a, String b) {
final da = DateTime.tryParse(a);
final db = DateTime.tryParse(b);
if (da == null || db == null) return null;
return da.difference(db).inDays;
}
/// Coarse bucket so fuzzy [isSameTransaction] only compares likely peers.
String _dedupeBucket(BankTransaction tx) =>
'${tx.date}|${tx.currency.toUpperCase()}|${_amountCents(tx.amount)}';
String _amountCurrencyBucket(BankTransaction tx) =>
'${tx.currency.toUpperCase()}|${_amountCents(tx.amount)}';
/// Drop pending rows that already have a booked twin (same money + merchant).
List<BankTransaction> dropSettledPendingDuplicates(
List<BankTransaction> txs,
) {
final booked = [for (final tx in txs) if (!tx.pending) tx];
if (booked.isEmpty) return txs;
final bookedBuckets = <String, List<BankTransaction>>{};
for (final tx in booked) {
bookedBuckets.putIfAbsent(_amountCurrencyBucket(tx), () => []).add(tx);
}
final out = <BankTransaction>[];
for (final tx in txs) {
if (!tx.pending || tx.isManual) {
out.add(tx);
continue;
}
final peers = bookedBuckets[_amountCurrencyBucket(tx)];
final settled =
peers != null && peers.any((b) => isPendingSettledBy(tx, b));
if (!settled) out.add(tx);
}
return out;
}
/// Merge a fresh bank API page with locally cached history.
///
/// Pending rows are ephemeral: only the current API list may keep them
/// (except admin manuals). That stops settled/cancelled pendings from
/// living forever in the on-device cache after the bank drops them.
List<BankTransaction> mergeApiTransactionsWithCache(
List<BankTransaction> apiTxs,
List<BankTransaction> cached,
) {
final history = [
for (final tx in cached)
if (!tx.pending || tx.isManual) tx,
];
return dropSettledPendingDuplicates(mergeTransactions(apiTxs, history));
}
/// Merge [extra] into [existing], skipping duplicates. Newest date first.
List<BankTransaction> mergeTransactions(
List<BankTransaction> existing,
+180 -37
View File
@@ -25,20 +25,6 @@ List<BankTransaction> mergeBankTransactionLists(
return mergeTransactions(local, remote);
}
/// Union Overview favourites by client id (local wins on conflict).
List<OverviewFavourite> mergeOverviewFavourites(
List<OverviewFavourite> local,
List<OverviewFavourite> remote,
) {
final byId = <String, OverviewFavourite>{
for (final fav in remote) fav.id: fav,
};
for (final fav in local) {
byId[fav.id] = fav;
}
return byId.values.toList(growable: false);
}
/// Union-merge so neither device loses annotations on first sync.
TxMetaStoreData mergeTxMetaStores(TxMetaStoreData a, TxMetaStoreData b) {
final byId = <String, TxMeta>{...a.byId};
@@ -485,6 +471,8 @@ class PbSyncService {
),
invertExpensePerspective:
r.getBoolValue('invert_expense_perspective'),
updatedAt: _recordUpdatedAtMs(r),
deleted: r.getBoolValue('deleted'),
),
);
}
@@ -544,6 +532,8 @@ class PbSyncService {
'showAmountIn': r.getBoolValue('show_amount_in', true),
'showAmountOut': r.getBoolValue('show_amount_out', true),
'showCount': r.getBoolValue('show_count', true),
'updatedAt': _recordUpdatedAtMs(r),
'deleted': r.getBoolValue('deleted'),
}),
);
}
@@ -598,10 +588,14 @@ class PbSyncService {
await _syncAppPrefs(userId, filter, prefs);
}
/// Upserts local bank txs to PocketBase. Does **not** delete remote rows
/// (partial local caches must not wipe another device).
/// Upserts local bank txs to PocketBase.
///
/// Also garbage-collects remote rows that are:
/// - admin test rows (`is_manual` / `·#…` stamp) no longer local, or
/// - listed in [deleteTombs] (local delete queue).
Future<String> pushTransactions(
List<BankTransaction> local, {
List<BankTransaction> deleteTombs = const [],
void Function(double progress)? onProgress,
void Function(String message)? onStatus,
double progressStart = 0.0,
@@ -630,10 +624,19 @@ class PbSyncService {
final ops = <_BatchOp>[];
var skipped = 0;
final seen = <String>{};
final tombIds = {
for (final t in deleteTombs)
if (_clip(t.id, 2000).isNotEmpty) _clip(t.id, 2000),
};
for (final tx in local) {
final syncId = _clip(tx.id, 2000);
if (syncId.isEmpty || !seen.add(syncId)) continue;
// Never re-upsert a tombstoned row in the same pass.
if (tombIds.contains(syncId) ||
deleteTombs.any(tx.matchesForDelete)) {
continue;
}
final body = <String, dynamic>{
'user': userId,
'tx_id': syncId,
@@ -648,7 +651,9 @@ class PbSyncService {
8,
),
'pending': tx.pending,
'is_manual': tx.isManual,
// Only send when true so servers without the migration still accept
// normal bank/CSV upserts. Test rows are also detected via ·# stamp.
if (tx.isManual) 'is_manual': true,
};
final remote = existing[syncId];
if (remote == null) {
@@ -660,11 +665,25 @@ class PbSyncService {
}
}
// Remove admin test / tombstoned rows that this device no longer has.
var deletes = 0;
for (final entry in existing.entries) {
if (seen.contains(entry.key)) continue;
final remoteTx = _bankTxFromRecord(entry.value);
final tombHit = tombIds.contains(entry.key) ||
(remoteTx != null && deleteTombs.any(remoteTx.matchesForDelete));
final orphanManual = _remoteLooksLikeManualTest(entry.value);
if (!tombHit && !orphanManual) continue;
ops.add(_BatchOp.delete(entry.value.id));
deletes++;
}
final creates = ops.where((o) => o.kind == _BatchOpKind.create).length;
final updates = ops.where((o) => o.kind == _BatchOpKind.update).length;
debugPrint(
'PbSyncService transactions: local=${local.length} '
'remote=${existing.length} create=$creates update=$updates skip=$skipped',
'remote=${existing.length} create=$creates update=$updates '
'delete=$deletes skip=$skipped',
);
onStatus?.call(
ops.isEmpty
@@ -678,8 +697,13 @@ class PbSyncService {
onStatus: onStatus,
);
return 'Bank txs: local ${local.length} → remote '
'${existing.length + creates} '
'(+$creates ~$updates · $skipped unchanged)';
'${existing.length + creates - deletes} '
'(+$creates ~$updates -$deletes · $skipped unchanged)';
}
bool _remoteLooksLikeManualTest(RecordModel r) {
if (r.getBoolValue('is_manual')) return true;
return RegExp(r' ·#\d+$').hasMatch(r.getStringValue('description'));
}
BankTransaction? _bankTxFromRecord(RecordModel r) {
@@ -700,7 +724,8 @@ class PbSyncService {
? 'EUR'
: r.getStringValue('currency'),
pending: r.getBoolValue('pending'),
isManual: r.getBoolValue('is_manual'),
isManual: r.getBoolValue('is_manual') ||
RegExp(r' ·#\d+$').hasMatch(description),
);
}
@@ -711,6 +736,8 @@ class PbSyncService {
String s => double.tryParse(s) ?? 0,
_ => 0.0,
};
final remoteManual = remote.getBoolValue('is_manual') ||
RegExp(r' ·#\d+$').hasMatch(remote.getStringValue('description'));
return remote.getStringValue('date') == tx.date &&
remote.getStringValue('description') ==
_clip(tx.description.isEmpty ? 'Unknown' : tx.description, 4000) &&
@@ -718,29 +745,117 @@ class PbSyncService {
remote.getStringValue('currency').toUpperCase() ==
tx.currency.toUpperCase() &&
remote.getBoolValue('pending') == tx.pending &&
remote.getBoolValue('is_manual') == tx.isManual;
remoteManual == tx.isManual;
}
/// Deletes remote bank rows by local [BankTransaction.id] (tx_id field).
Future<String> deleteTransactionsByTxIds(Iterable<String> txIds) async {
final ids = {
for (final id in txIds)
if (_clip(id, 2000).isNotEmpty) _clip(id, 2000),
};
if (ids.isEmpty) return 'Bank txs: nothing to delete';
/// Deletes remote bank rows matching [tombstones] by tx_id or field fingerprint.
///
/// Returns the tombstones that matched at least one remote row (safe to clear).
Future<List<BankTransaction>> deleteTransactionsMatching(
List<BankTransaction> tombstones,
) async {
if (tombstones.isEmpty) return const [];
final userId = _userId;
final filter = 'user="$userId"';
final existing = await _fullList('transactions', filter);
final ops = <_BatchOp>[
for (final r in existing)
if (ids.contains(r.getStringValue('tx_id'))) _BatchOp.delete(r.id),
];
final matchedTombs = <BankTransaction>[];
final ops = <_BatchOp>[];
final seenRecordIds = <String>{};
for (final r in existing) {
final remoteTx = _bankTxFromRecord(r);
if (remoteTx == null) continue;
for (final tomb in tombstones) {
final txId = r.getStringValue('tx_id');
final idHit = txId == _clip(tomb.id, 2000);
if (!idHit && !remoteTx.matchesForDelete(tomb)) continue;
if (!seenRecordIds.add(r.id)) break;
ops.add(_BatchOp.delete(r.id));
if (!matchedTombs.any(tomb.matchesForDelete)) {
matchedTombs.add(tomb);
}
break;
}
}
if (ops.isEmpty) {
return 'Bank txs: 0 remote deletes (ids ${ids.length})';
debugPrint(
'PbSyncService deleteTransactionsMatching: 0 hits '
'for ${tombstones.length} tombstone(s)',
);
return const [];
}
await _runWriteOps('transactions', ops);
return 'Bank txs: deleted ${ops.length} remote row(s)';
debugPrint(
'PbSyncService deleteTransactionsMatching: deleted ${ops.length} '
'for ${matchedTombs.length} tombstone(s)',
);
return matchedTombs;
}
/// Pull synced delete tombstones for admin test transactions.
Future<List<BankTransaction>> pullTxDeletes({
void Function(String message)? onStatus,
}) async {
final userId = _userId;
final filter = 'user="$userId"';
onStatus?.call('Pulling deleted test txs…');
final records = await _fullList('tx_deletes', filter);
final out = <BankTransaction>[];
for (final r in records) {
final tx = _bankTxFromRecord(r);
if (tx == null) continue;
out.add(tx.copyWith(isManual: true));
}
debugPrint('PbSyncService pull: tx_deletes=${out.length}');
return out;
}
/// Upsert local delete tombstones. Never removes remote-only rows (union).
Future<String> pushTxDeletes(
List<BankTransaction> tombs, {
void Function(String message)? onStatus,
}) async {
if (tombs.isEmpty) return 'Tx deletes: nothing to push';
final userId = _userId;
final filter = 'user="$userId"';
onStatus?.call('Syncing deleted test txs…');
final existing = {
for (final r in await _fullList('tx_deletes', filter))
r.getStringValue('tx_id'): r,
};
final ops = <_BatchOp>[];
var skipped = 0;
final seen = <String>{};
for (final tomb in tombs) {
final syncId = _clip(tomb.id, 2000);
if (syncId.isEmpty || !seen.add(syncId)) continue;
final body = <String, dynamic>{
'user': userId,
'tx_id': syncId,
'date': _clip(tomb.date, 32),
'description': _clip(
tomb.description.isEmpty ? 'Unknown' : tomb.description,
4000,
),
'amount': tomb.amount,
'currency': _clip(
tomb.currency.isEmpty ? 'EUR' : tomb.currency.toUpperCase(),
8,
),
};
final remote = existing[syncId];
if (remote == null) {
ops.add(_BatchOp.create(body));
} else {
skipped++;
}
}
await _runWriteOps('tx_deletes', ops, onStatus: onStatus);
return 'Tx deletes: local ${tombs.length} (+${ops.length} · $skipped exist)';
}
Future<void> _runOne(String collection, _BatchOp op) {
@@ -907,6 +1022,12 @@ class PbSyncService {
filter =
'user="$userId" && tx_id="${key.replaceAll('"', '\\"')}"';
}
case 'tx_deletes':
final key = body['tx_id'];
if (userId != null && key is String) {
filter =
'user="$userId" && tx_id="${key.replaceAll('"', '\\"')}"';
}
}
if (filter == null) {
throw StateError('Cannot upsert $collection without filter');
@@ -1233,6 +1354,8 @@ class PbSyncService {
'flow': fav.flow.name,
'bar_style': fav.barStyle.name,
'invert_expense_perspective': fav.invertExpensePerspective,
'updated_at': fav.updatedAt,
'deleted': fav.deleted,
};
final remote = existing[clientId];
if (remote == null) {
@@ -1297,6 +1420,8 @@ class PbSyncService {
'show_amount_in': w.showAmountIn,
'show_amount_out': w.showAmountOut,
'show_count': w.showCount,
'updated_at': w.updatedAt,
'deleted': w.deleted,
};
final remote = existing[clientId];
if (remote == null) {
@@ -1402,7 +1527,9 @@ class PbSyncService {
remote.getStringValue('flow') == fav.flow.name &&
remote.getStringValue('bar_style') == fav.barStyle.name &&
remote.getBoolValue('invert_expense_perspective') ==
fav.invertExpensePerspective;
fav.invertExpensePerspective &&
remote.getBoolValue('deleted') == fav.deleted &&
_recordUpdatedAtMs(remote) == fav.updatedAt;
}
bool _homeFilterWidgetUnchanged(RecordModel remote, HomeFilterWidget w) {
@@ -1441,7 +1568,9 @@ class PbSyncService {
remote.getBoolValue('month_only') == w.monthOnly &&
remote.getBoolValue('show_amount_in', true) == w.showAmountIn &&
remote.getBoolValue('show_amount_out', true) == w.showAmountOut &&
remote.getBoolValue('show_count', true) == w.showCount;
remote.getBoolValue('show_count', true) == w.showCount &&
remote.getBoolValue('deleted') == w.deleted &&
_recordUpdatedAtMs(remote) == w.updatedAt;
}
bool _appPrefsUnchanged(RecordModel remote, AppCloudPrefs prefs) {
@@ -1454,6 +1583,20 @@ class PbSyncService {
remote.getIntValue('look_ahead_max_years', 5) ==
prefs.lookAheadMaxYears.clamp(1, 30);
}
/// Prefer custom `updated_at`, else PocketBase system `updated`.
int _recordUpdatedAtMs(RecordModel r) {
final raw = r.get<dynamic>('updated_at');
final custom = switch (raw) {
int n => n,
num n => n.round(),
String s => int.tryParse(s) ?? 0,
_ => 0,
};
if (custom > 0) return custom;
final system = DateTime.tryParse(r.getStringValue('updated'));
return system?.millisecondsSinceEpoch ?? 0;
}
}
RecurringTemplateSource _parseTemplateSource(String raw) {
+47
View File
@@ -260,6 +260,53 @@ class StorageService {
return _prefs.remove(StorageKeys.importedTransactions);
}
/// Tombstones for locally deleted bank txs awaiting remote removal.
///
/// Supports legacy string-id lists and full transaction JSON objects.
List<BankTransaction> loadPendingTxDeletes() {
final raw = _prefs.getString(StorageKeys.pendingTxDeletes);
if (raw == null || raw.isEmpty) return const [];
try {
final decoded = jsonDecode(raw);
if (decoded is! List) return const [];
final out = <BankTransaction>[];
for (final e in decoded) {
if (e is Map) {
out.add(BankTransaction.fromJson(Map<String, dynamic>.from(e)));
} else if (e is String && e.isNotEmpty) {
// Legacy id-only entry — match by id string only.
final parts = e.split('|');
if (parts.length >= 4) {
final amount = double.tryParse(parts[2]) ?? 0;
out.add(
BankTransaction(
date: parts[0],
currency: parts[1],
amount: amount,
description: parts.sublist(3).join('|'),
pending: false,
isManual: true,
),
);
}
}
}
return out;
} catch (_) {
return const [];
}
}
Future<void> savePendingTxDeletes(List<BankTransaction> txs) {
if (txs.isEmpty) {
return _prefs.remove(StorageKeys.pendingTxDeletes);
}
return _prefs.setString(
StorageKeys.pendingTxDeletes,
jsonEncode([for (final tx in txs) tx.toJson()]),
);
}
TxMetaStoreData loadTxMetaStore() {
final raw = _prefs.getString(StorageKeys.txMeta);
if (raw != null && raw.isNotEmpty) {
+3
View File
@@ -46,6 +46,9 @@ abstract final class StorageKeys {
static const bankCache = 'bank_cache_v1';
static const importedTransactions = 'imported_txs_v1';
/// Local bank tx_ids queued for PocketBase deletion (JSON string list).
static const pendingTxDeletes = 'pending_tx_deletes_v1';
/// Serialized PocketBase auth token + user record (separate from tx data).
static const pocketBaseAuth = 'pb_auth_v1';
+50
View File
@@ -0,0 +1,50 @@
/// Schedule heavy tab work off the first paint, yielding so nav stays responsive.
library;
import 'dart:async';
/// Runs [compute] after a frame, cancellable via [cancel] / generation bumps.
class DeferredTabWork<T> {
int _gen = 0;
Object? _loadingKey;
T? value;
Object? valueKey;
bool readyFor(Object key) => value != null && valueKey == key;
bool get isLoading => _loadingKey != null;
void cancel() {
_gen++;
_loadingKey = null;
}
/// Starts work for [key] unless already ready or in flight for that key.
void ensure({
required Object key,
required Future<T> Function(Future<void> Function() yieldUi) compute,
required void Function() onUpdate,
required bool Function() mounted,
}) {
if (readyFor(key) || _loadingKey == key) return;
_loadingKey = key;
final gen = ++_gen;
unawaited(() async {
await Future<void>.delayed(Duration.zero);
if (!mounted() || gen != _gen) return;
Future<void> yieldUi() async {
await Future<void>.delayed(Duration.zero);
}
final result = await compute(yieldUi);
if (!mounted() || gen != _gen) return;
value = result;
valueKey = key;
if (_loadingKey == key) _loadingKey = null;
onUpdate();
}());
}
}
+59 -16
View File
@@ -23,6 +23,8 @@ class HomeFilterWidgetCard extends StatelessWidget {
this.onToggleDisplay,
this.onEdit,
this.dragHandle,
this.precomputedStats,
this.precomputedBudget,
});
final HomeFilterWidget widgetConfig;
@@ -31,6 +33,10 @@ class HomeFilterWidgetCard extends StatelessWidget {
final VoidCallback? onEdit;
final Widget? dragHandle;
/// When set, skips scanning all transactions in build.
final TxFilterMetrics? precomputedStats;
final HomeBudgetMetrics? precomputedBudget;
void _openTransactions(BuildContext context) {
final filter = widgetConfig.filter;
Navigator.of(context).push(
@@ -95,12 +101,14 @@ class HomeFilterWidgetCard extends StatelessWidget {
currency: currency,
transactions: bank.transactions,
meta: meta,
precomputed: precomputedBudget,
)
: _StatsBody(
widgetConfig: widgetConfig,
currency: currency,
transactions: bank.transactions,
meta: meta,
precomputed: precomputedStats,
),
),
if (editing && onToggleDisplay != null)
@@ -267,18 +275,21 @@ class _StatsBody extends StatelessWidget {
required this.currency,
required this.transactions,
required this.meta,
this.precomputed,
});
final HomeFilterWidget widgetConfig;
final String currency;
final List<BankTransaction> transactions;
final TxMetaController meta;
final TxFilterMetrics? precomputed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final metrics = computeTxFilterMetrics(
final metrics = precomputed ??
computeTxFilterMetrics(
all: transactions,
meta: meta,
filter: widgetConfig.filter,
@@ -288,19 +299,14 @@ class _StatsBody extends StatelessWidget {
final spentOnly = widgetConfig.showAmountOut &&
!widgetConfig.showAmountIn &&
!widgetConfig.showCount;
final half = widgetConfig.halfWidth;
// Avoid LayoutBuilder/Spacer/Expanded here: half-width rows use
// IntrinsicHeight, which cannot measure those widgets.
Widget metricsBlock({required bool alignEnd}) {
if (spentOnly) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
alignEnd ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
_CardHeader(
icon: widgetConfig.icon,
title: widgetConfig.name,
subtitle: _subtitleFor(widgetConfig),
),
if (spentOnly) ...[
const SizedBox(height: 32),
Text(
'Spent',
style: theme.textTheme.labelMedium?.copyWith(
@@ -316,11 +322,13 @@ class _StatsBody extends StatelessWidget {
height: 1.15,
),
),
] else ...[
const SizedBox(height: 14),
Wrap(
],
);
}
return Wrap(
spacing: 16,
runSpacing: 8,
alignment: alignEnd ? WrapAlignment.end : WrapAlignment.start,
children: [
if (widgetConfig.showAmountIn)
_Metric(
@@ -336,13 +344,45 @@ class _StatsBody extends StatelessWidget {
),
if (widgetConfig.showCount)
_Metric(
label: 'Txs',
label: 'Transactions',
value: '${metrics.count}',
color: colors.primary,
),
],
);
}
// Avoid LayoutBuilder/Spacer/Expanded here: half-width rows use
// IntrinsicHeight, which cannot measure those widgets.
if (!half) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _CardHeader(
icon: widgetConfig.icon,
title: widgetConfig.name,
subtitle: _subtitleFor(widgetConfig),
),
),
const SizedBox(width: 12),
Flexible(
child: metricsBlock(alignEnd: true),
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CardHeader(
icon: widgetConfig.icon,
title: widgetConfig.name,
subtitle: _subtitleFor(widgetConfig),
),
SizedBox(height: spentOnly ? 32 : 14),
metricsBlock(alignEnd: false),
],
);
}
@@ -354,18 +394,21 @@ class _BudgetBody extends StatelessWidget {
required this.currency,
required this.transactions,
required this.meta,
this.precomputed,
});
final HomeFilterWidget widgetConfig;
final String currency;
final List<BankTransaction> transactions;
final TxMetaController meta;
final HomeBudgetMetrics? precomputed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final budget = computeHomeBudgetMetrics(
final budget = precomputed ??
computeHomeBudgetMetrics(
all: transactions,
meta: meta,
widget: widgetConfig,
@@ -0,0 +1,83 @@
/// PocketBase migration: `tx_deletes` — synced tombstones for deleted admin
/// test transactions so another device cannot push them back into `transactions`.
///
/// Apply on the server:
///
/// sudo cp this_file /opt/pocketbase/pb_migrations/
/// sudo chown pocketbase:pocketbase /opt/pocketbase/pb_migrations/*.js
/// sudo -u pocketbase /opt/pocketbase/pocketbase migrate up
/// sudo systemctl restart pocketbase
migrate((app) => {
const users = app.findCollectionByNameOrId("usersapp");
try {
app.findCollectionByNameOrId("tx_deletes");
return;
} catch (_) {}
const ownerRules = {
listRule: "user = @request.auth.id",
viewRule: "user = @request.auth.id",
createRule: "@request.auth.id != '' && user = @request.auth.id",
updateRule: "user = @request.auth.id",
deleteRule: "user = @request.auth.id",
};
const txDeletes = new Collection({
type: "base",
name: "tx_deletes",
...ownerRules,
fields: [
{
type: "relation",
name: "user",
required: true,
collectionId: users.id,
cascadeDelete: true,
maxSelect: 1,
},
{
// Clipped BankTransaction.id
type: "text",
name: "tx_id",
required: true,
min: 1,
max: 2000,
},
{
type: "text",
name: "date",
required: true,
min: 1,
max: 32,
},
{
type: "text",
name: "description",
required: true,
min: 1,
max: 4000,
},
{
type: "number",
name: "amount",
required: true,
},
{
type: "text",
name: "currency",
required: true,
min: 1,
max: 8,
},
],
indexes: [
"CREATE UNIQUE INDEX idx_tx_deletes_user_tx ON tx_deletes (user, tx_id)",
],
});
app.save(txDeletes);
}, (app) => {
try {
app.delete(app.findCollectionByNameOrId("tx_deletes"));
} catch (_) {}
});

Some files were not shown because too many files have changed in this diff Show More