Refine backendless share intake
This commit is contained in:
@@ -40,6 +40,7 @@ app.*.map.json
|
||||
# Android
|
||||
# -----------------------------
|
||||
android/.gradle/
|
||||
android/local.properties
|
||||
android/app/debug/
|
||||
android/app/profile/
|
||||
android/app/release/
|
||||
@@ -49,8 +50,15 @@ android/app/release/
|
||||
# -----------------------------
|
||||
ios/Flutter/ephemeral/
|
||||
ios/Flutter/.last_build_id
|
||||
ios/Flutter/Flutter.podspec
|
||||
ios/Flutter/Generated.xcconfig
|
||||
ios/Flutter/flutter_export_environment.sh
|
||||
ios/Flutter/App.framework/
|
||||
ios/Flutter/Flutter.framework/
|
||||
ios/Pods/
|
||||
ios/.symlinks/
|
||||
ios/Runner/GeneratedPluginRegistrant.h
|
||||
ios/Runner/GeneratedPluginRegistrant.m
|
||||
ios/Runner.xcworkspace/xcuserdata/
|
||||
ios/Runner.xcodeproj/xcuserdata/
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# ADR 0003: Backendless Local LLM Pipeline
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The product goal is a phone-first relationship memory app. The user shares
|
||||
messages and text snippets from messaging apps, resolves uncertain identity
|
||||
matches over time, and receives private extraction plus useful suggestions.
|
||||
|
||||
The app is moving away from a server-backed source of truth. Backend sync and
|
||||
the fake backend should not create or overwrite relationship data unless a
|
||||
developer explicitly enables REST sync for transport work.
|
||||
|
||||
iOS does not provide a reliable always-running background service. Background
|
||||
work must be treated as opportunistic: run on foreground/app resume, after share
|
||||
intake, through user-initiated actions, and through scheduled background windows
|
||||
when the OS grants time.
|
||||
|
||||
## Decision
|
||||
|
||||
Use a backendless local-first pipeline:
|
||||
|
||||
1. Share intake stores raw shared payloads locally.
|
||||
2. Strong identity matches attach directly to an existing person.
|
||||
3. Ambiguous, conflicting, or low-information shares stay in `Share Inbox`.
|
||||
4. Nightly extraction processes only new/resolved share batches.
|
||||
5. Weekly/on-demand recommendations use compact profile summaries plus prior
|
||||
suggestion history to avoid repeats.
|
||||
6. Internet-grounded recommendations are generated only during the weekly or
|
||||
manual recommendation phase, not during nightly extraction.
|
||||
7. Fake backend sync is inert by default and must not seed local relationship
|
||||
records.
|
||||
|
||||
## LLM Cost Controls
|
||||
|
||||
- Batch new shares into one extraction request per run.
|
||||
- Use source fingerprints and content hashes to skip repeated extraction.
|
||||
- Store extraction run fingerprints, completed timestamps, and failures.
|
||||
- Send compact person tokens and normalized facts instead of names and raw
|
||||
history when possible.
|
||||
- Keep grounded shopping/event prompts separate from nightly extraction because
|
||||
web search is more expensive and time-sensitive.
|
||||
- Include accepted/dismissed/pending suggestion fingerprints in weekly prompts
|
||||
so repeated concerts, shops, or gifts are suppressed.
|
||||
|
||||
## iOS Background Policy
|
||||
|
||||
- Treat scheduled nightly and weekly work as best-effort.
|
||||
- Always offer manual `Run Now` actions.
|
||||
- Prefer local notifications after work completes.
|
||||
- Do not require APNs or a server daemon for core behavior.
|
||||
- Keep work units short enough to survive iOS background expiration.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The app remains useful without any backend.
|
||||
- Share capture and profile building are durable across weeks/months of gradual
|
||||
user review.
|
||||
- Suggestions may run later than the configured wall-clock time on iOS.
|
||||
- REST sync can still exist as a developer/integration path, but it is not part
|
||||
of the primary product loop.
|
||||
+23
-2
@@ -25,7 +25,8 @@ flutter run --dart-define=USE_FAKE_BACKEND=true
|
||||
## Background Sync
|
||||
|
||||
Background sync (startup + resume + periodic ticks while authenticated) is
|
||||
enabled by default.
|
||||
disabled by default. The current product direction is backendless/local-first,
|
||||
so backend sync should be enabled only for REST transport development.
|
||||
|
||||
In REST mode, auto-triggers are reachability-gated to avoid unnecessary sync
|
||||
attempts while offline.
|
||||
@@ -78,7 +79,23 @@ Behavior notes:
|
||||
## Phone-Only Private AI Digest
|
||||
|
||||
The app can run without a production backend by keeping `USE_FAKE_BACKEND=true`
|
||||
and using the LLM integration only for a private weekly digest.
|
||||
and using the LLM integration for local profile extraction and private digest
|
||||
generation.
|
||||
|
||||
Selected product flow:
|
||||
|
||||
- share messages/text from WhatsApp, iMessage, Notes, Safari, or similar apps
|
||||
into the app
|
||||
- auto-match only when identity evidence is strong
|
||||
- keep ambiguous or low-information shares in `Share Inbox`
|
||||
- let the user gradually resolve inbox items over days, weeks, and months
|
||||
- run local/profile-building extraction opportunistically and on scheduled
|
||||
nightly windows when the OS allows it
|
||||
- run grounded weekly recommendations on request or on the configured digest
|
||||
schedule
|
||||
|
||||
Backend sync is not part of this path. The fake backend is intentionally inert
|
||||
for sync pulls and must not seed people or other relationship records.
|
||||
|
||||
Digest behavior:
|
||||
|
||||
@@ -86,6 +103,10 @@ Digest behavior:
|
||||
- scheduled digest payloads use pseudonymous tokens such as `person_001`
|
||||
- names, aliases, sender names, source URLs, raw shared text, and sensitive
|
||||
notes are not sent in the scheduled digest prompt
|
||||
- repeated LLM work should be avoided by fingerprinting unresolved/new share
|
||||
batches and skipping extraction when the same batch has already completed
|
||||
- weekly recommendation prompts should include previously suggested/dismissed
|
||||
items so the model avoids repeats
|
||||
- LLM results are saved as pending AI review drafts first
|
||||
- accepting a draft creates a local idea, task, or reminder
|
||||
- dismissing a draft leaves existing local data unchanged
|
||||
|
||||
@@ -32,9 +32,6 @@ target 'Runner' do
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'Share Extension' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
+1
-1
@@ -75,6 +75,6 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
PODFILE CHECKSUM: 09dd68ff20d1f7e1497be5a3d807622c3053984a
|
||||
PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
15BE7299C8BED38E2BC0A6A0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C88FBC0E3BAB70074730065 /* Pods_Runner.framework */; };
|
||||
A1B2C3011111111111111111 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */; };
|
||||
A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; };
|
||||
A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; };
|
||||
A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
5E1E9B720DF095CAC1A2FF8A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */; };
|
||||
@@ -21,16 +17,12 @@
|
||||
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 */; };
|
||||
A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; };
|
||||
A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; };
|
||||
A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
A1B2C3051111111111111111 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = A1B2C3131111111111111111;
|
||||
remoteInfo = "Share Extension";
|
||||
};
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
@@ -38,9 +30,26 @@
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
A1B2C3051111111111111111 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = A1B2C3131111111111111111;
|
||||
remoteInfo = "Share Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A1B2C3061111111111111111 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -52,32 +61,12 @@
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E770A251CA9930160492DC4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B2C30A1111111111111111 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A1B2C30C1111111111111111 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A1B2C30D1111111111111111 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; 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>"; };
|
||||
@@ -96,20 +85,18 @@
|
||||
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>"; };
|
||||
9B7CFEE2CE4B7B1DCB85B3E1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B2C30A1111111111111111 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
A1B2C30C1111111111111111 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A1B2C30D1111111111111111 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
|
||||
CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A1B2C3111111111111111111 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3011111111111111111 /* Pods_Share_Extension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -118,6 +105,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A1B2C3111111111111111111 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E2BAEB33085D4BFE88A55D45 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -143,14 +137,10 @@
|
||||
E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */,
|
||||
9B7CFEE2CE4B7B1DCB85B3E1 /* Pods-Runner.release.xcconfig */,
|
||||
0E770A251CA9930160492DC4 /* Pods-Runner.profile.xcconfig */,
|
||||
A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */,
|
||||
A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */,
|
||||
A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */,
|
||||
F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */,
|
||||
9708213885153303B111D6D9 /* Pods-RunnerTests.release.xcconfig */,
|
||||
6881917E7D40106AEFE87622 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -158,7 +148,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C88FBC0E3BAB70074730065 /* Pods_Runner.framework */,
|
||||
A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */,
|
||||
CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
@@ -229,24 +218,6 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
A1B2C3131111111111111111 /* Share Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */;
|
||||
buildPhases = (
|
||||
A1B2C3151111111111111111 /* [CP] Check Pods Manifest.lock */,
|
||||
A1B2C3161111111111111111 /* Sources */,
|
||||
A1B2C3111111111111111111 /* Frameworks */,
|
||||
A1B2C3141111111111111111 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Share Extension";
|
||||
productName = "Share Extension";
|
||||
productReference = A1B2C30C1111111111111111 /* Share Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
@@ -290,6 +261,23 @@
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
A1B2C3131111111111111111 /* Share Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */;
|
||||
buildPhases = (
|
||||
A1B2C3161111111111111111 /* Sources */,
|
||||
A1B2C3111111111111111111 /* Frameworks */,
|
||||
A1B2C3141111111111111111 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Share Extension";
|
||||
productName = "Share Extension";
|
||||
productReference = A1B2C30C1111111111111111 /* Share Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -300,9 +288,6 @@
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
A1B2C3131111111111111111 = {
|
||||
CreatedOnToolsVersion = 15.0.1;
|
||||
};
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
@@ -311,6 +296,9 @@
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
A1B2C3131111111111111111 = {
|
||||
CreatedOnToolsVersion = 15.0.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
@@ -334,14 +322,6 @@
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
A1B2C3141111111111111111 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -360,31 +340,17 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A1B2C3141111111111111111 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
A1B2C3151111111111111111 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Share Extension-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
38116227E31B7524BBB62C35 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -480,14 +446,6 @@
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
A1B2C3161111111111111111 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3021111111111111111 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -506,30 +464,30 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A1B2C3161111111111111111 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3021111111111111111 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
A1B2C3171111111111111111 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A1B2C3131111111111111111 /* Share Extension */;
|
||||
targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */;
|
||||
};
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
A1B2C3171111111111111111 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A1B2C3131111111111111111 /* Share Extension */;
|
||||
targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
A1B2C3181111111111111111 /* MainInterface.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
A1B2C30E1111111111111111 /* Base */,
|
||||
);
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -546,18 +504,26 @@
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1B2C3181111111111111111 /* MainInterface.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
A1B2C30E1111111111111111 /* Base */,
|
||||
);
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver;
|
||||
APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared";
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
|
||||
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -584,10 +550,10 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -608,122 +574,40 @@
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
A1B2C3191111111111111111 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A1B2C31A1111111111111111 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A1B2C31B1111111111111111 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
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";
|
||||
@@ -733,15 +617,15 @@
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9708213885153303B111D6D9 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
@@ -750,27 +634,27 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6881917E7D40106AEFE87622 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)";
|
||||
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 = {
|
||||
APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver;
|
||||
APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared";
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
|
||||
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -797,10 +681,10 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
@@ -824,15 +708,15 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver;
|
||||
APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared";
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
|
||||
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
|
||||
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
|
||||
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -859,10 +743,10 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM_ID = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -885,23 +769,23 @@
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
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";
|
||||
@@ -910,41 +794,110 @@
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A1B2C3191111111111111111 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A1B2C31A1111111111111111 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A1B2C31B1111111111111111 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
|
||||
DEVELOPMENT_TEAM = SR3UKQVKP6;
|
||||
INFOPLIST_FILE = "Share Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A1B2C3191111111111111111 /* Debug */,
|
||||
A1B2C31A1111111111111111 /* Release */,
|
||||
A1B2C31B1111111111111111 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -975,6 +928,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A1B2C3191111111111111111 /* Debug */,
|
||||
A1B2C31A1111111111111111 /* Release */,
|
||||
A1B2C31B1111111111111111 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.relationshipsaver.llm.digest</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@@ -12,8 +18,6 @@
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -24,8 +28,6 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -37,6 +39,8 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
@@ -69,10 +73,6 @@
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.relationshipsaver.llm.digest</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -4,30 +4,44 @@
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Relationship Saver</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>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>20</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>10</integer>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>PHSupportedMediaTypes</key>
|
||||
<array>
|
||||
<string>Video</string>
|
||||
<string>Image</string>
|
||||
</array>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>20</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>10</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
|
||||
@@ -1,3 +1,315 @@
|
||||
import receive_sharing_intent
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
class ShareViewController: RSIShareViewController {}
|
||||
private let schemePrefix = "ShareMedia"
|
||||
private let userDefaultsKey = "ShareKey"
|
||||
private let userDefaultsMessageKey = "ShareMessageKey"
|
||||
private let appGroupIdKey = "AppGroupId"
|
||||
|
||||
private struct SharedMediaFile: Codable {
|
||||
let path: String
|
||||
let mimeType: String?
|
||||
let thumbnail: String?
|
||||
let duration: Double?
|
||||
let message: String?
|
||||
let type: SharedMediaType
|
||||
}
|
||||
|
||||
private enum SharedMediaType: String, Codable, CaseIterable {
|
||||
case image
|
||||
case video
|
||||
case text
|
||||
case file
|
||||
case url
|
||||
|
||||
var typeIdentifier: String {
|
||||
if #available(iOS 14.0, *) {
|
||||
switch self {
|
||||
case .image:
|
||||
return UTType.image.identifier
|
||||
case .video:
|
||||
return UTType.movie.identifier
|
||||
case .text:
|
||||
return UTType.text.identifier
|
||||
case .file:
|
||||
return UTType.fileURL.identifier
|
||||
case .url:
|
||||
return UTType.url.identifier
|
||||
}
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .image:
|
||||
return kUTTypeImage as String
|
||||
case .video:
|
||||
return kUTTypeMovie as String
|
||||
case .text:
|
||||
return kUTTypeText as String
|
||||
case .file:
|
||||
return kUTTypeFileURL as String
|
||||
case .url:
|
||||
return kUTTypeURL as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ShareViewController: SLComposeServiceViewController {
|
||||
private var hostAppBundleIdentifier = ""
|
||||
private var appGroupId = ""
|
||||
private var sharedMedia: [SharedMediaFile] = []
|
||||
private let group = DispatchGroup()
|
||||
private var didFinishLoadingAttachments = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadIdentifiers()
|
||||
}
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func didSelectPost() {
|
||||
appendContentTextIfNeeded()
|
||||
saveAndRedirect(message: contentText)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
guard !didFinishLoadingAttachments else {
|
||||
return
|
||||
}
|
||||
didFinishLoadingAttachments = true
|
||||
loadAttachments()
|
||||
}
|
||||
|
||||
override func configurationItems() -> [Any]! {
|
||||
[]
|
||||
}
|
||||
|
||||
private func loadIdentifiers() {
|
||||
let shareExtensionIdentifier = Bundle.main.bundleIdentifier ?? ""
|
||||
if let lastDotIndex = shareExtensionIdentifier.lastIndex(of: ".") {
|
||||
hostAppBundleIdentifier = String(shareExtensionIdentifier[..<lastDotIndex])
|
||||
} else {
|
||||
hostAppBundleIdentifier = shareExtensionIdentifier
|
||||
}
|
||||
|
||||
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
|
||||
appGroupId =
|
||||
Bundle.main.object(forInfoDictionaryKey: appGroupIdKey) as? String
|
||||
?? defaultAppGroupId
|
||||
}
|
||||
|
||||
private func loadAttachments() {
|
||||
guard
|
||||
let inputItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||
let attachments = inputItem.attachments
|
||||
else {
|
||||
appendContentTextIfNeeded()
|
||||
saveAndRedirect(message: contentText)
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("RelationshipSaverShare: loading %d attachment(s)", attachments.count)
|
||||
for attachment in attachments {
|
||||
loadAttachment(attachment)
|
||||
}
|
||||
|
||||
group.notify(queue: .main) { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.appendContentTextIfNeeded()
|
||||
if !self.sharedMedia.isEmpty {
|
||||
self.saveAndRedirect()
|
||||
} else {
|
||||
NSLog("RelationshipSaverShare: no shareable media or text found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAttachment(_ attachment: NSItemProvider) {
|
||||
for type in SharedMediaType.allCases where attachment.hasItemConformingToTypeIdentifier(type.typeIdentifier) {
|
||||
group.enter()
|
||||
attachment.loadItem(forTypeIdentifier: type.typeIdentifier, options: nil) { [weak self] item, _ in
|
||||
defer {
|
||||
self?.group.leave()
|
||||
}
|
||||
self?.appendSharedItem(item, type: type)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func appendSharedItem(_ item: NSSecureCoding?, type: SharedMediaType) {
|
||||
switch (item, type) {
|
||||
case let (text as String, .text):
|
||||
appendLiteral(text, type: .text, mimeType: "text/plain")
|
||||
case let (url as URL, .url):
|
||||
appendLiteral(url.absoluteString, type: .url, mimeType: nil)
|
||||
case let (url as URL, _):
|
||||
appendFile(url, type: type)
|
||||
case let (image as UIImage, .image):
|
||||
appendImage(image)
|
||||
default:
|
||||
NSLog("RelationshipSaverShare: unsupported item for type %@", type.rawValue)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func appendContentTextIfNeeded() {
|
||||
let trimmed = contentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
return
|
||||
}
|
||||
guard !sharedMedia.contains(where: { $0.type == .text && $0.path == trimmed }) else {
|
||||
return
|
||||
}
|
||||
NSLog("RelationshipSaverShare: using compose contentText fallback length=%d", trimmed.count)
|
||||
appendLiteral(trimmed, type: .text, mimeType: "text/plain")
|
||||
}
|
||||
|
||||
private func appendLiteral(_ value: String, type: SharedMediaType, mimeType: String?) {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
sharedMedia.append(
|
||||
SharedMediaFile(
|
||||
path: trimmed,
|
||||
mimeType: mimeType,
|
||||
thumbnail: nil,
|
||||
duration: nil,
|
||||
message: nil,
|
||||
type: type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func appendFile(_ sourceUrl: URL, type: SharedMediaType) {
|
||||
guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
||||
return
|
||||
}
|
||||
|
||||
let fileName = sourceUrl.lastPathComponent.isEmpty ? UUID().uuidString : sourceUrl.lastPathComponent
|
||||
let destinationUrl = containerUrl.appendingPathComponent(fileName)
|
||||
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: destinationUrl.path) {
|
||||
try FileManager.default.removeItem(at: destinationUrl)
|
||||
}
|
||||
try FileManager.default.copyItem(at: sourceUrl, to: destinationUrl)
|
||||
sharedMedia.append(
|
||||
SharedMediaFile(
|
||||
path: destinationUrl.absoluteString.removingPercentEncoding ?? destinationUrl.absoluteString,
|
||||
mimeType: sourceUrl.mimeType,
|
||||
thumbnail: nil,
|
||||
duration: nil,
|
||||
message: nil,
|
||||
type: type
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func appendImage(_ image: UIImage) {
|
||||
guard
|
||||
let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId),
|
||||
let imageData = image.pngData()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let destinationUrl = containerUrl.appendingPathComponent("\(UUID().uuidString).png")
|
||||
|
||||
do {
|
||||
try imageData.write(to: destinationUrl)
|
||||
sharedMedia.append(
|
||||
SharedMediaFile(
|
||||
path: destinationUrl.absoluteString.removingPercentEncoding ?? destinationUrl.absoluteString,
|
||||
mimeType: "image/png",
|
||||
thumbnail: nil,
|
||||
duration: nil,
|
||||
message: nil,
|
||||
type: .image
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAndRedirect(message: String? = nil) {
|
||||
guard let encodedData = try? JSONEncoder().encode(sharedMedia) else {
|
||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let userDefaults = UserDefaults(suiteName: appGroupId)
|
||||
userDefaults?.set(encodedData, forKey: userDefaultsKey)
|
||||
userDefaults?.set(message, forKey: userDefaultsMessageKey)
|
||||
userDefaults?.synchronize()
|
||||
NSLog("RelationshipSaverShare: saved %d shared item(s), appGroup=%@", sharedMedia.count, appGroupId)
|
||||
redirectToHostApp()
|
||||
}
|
||||
|
||||
private func redirectToHostApp() {
|
||||
loadIdentifiers()
|
||||
guard let url = URL(string: "\(schemePrefix)-\(hostAppBundleIdentifier):share") else {
|
||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
return
|
||||
}
|
||||
|
||||
var responder: UIResponder? = self
|
||||
let selectorOpenUrl = sel_registerName("openURL:")
|
||||
|
||||
if #available(iOS 18.0, *) {
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
application.open(url, options: [:], completionHandler: nil)
|
||||
break
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
} else {
|
||||
while responder != nil {
|
||||
if responder?.responds(to: selectorOpenUrl) == true {
|
||||
_ = responder?.perform(selectorOpenUrl, with: url)
|
||||
break
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
}
|
||||
|
||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL {
|
||||
var mimeType: String {
|
||||
if #available(iOS 14.0, *),
|
||||
let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType
|
||||
{
|
||||
return mimeType
|
||||
}
|
||||
|
||||
guard
|
||||
let identifier = UTTypeCreatePreferredIdentifierForTag(
|
||||
kUTTagClassFilenameExtension,
|
||||
pathExtension as NSString,
|
||||
nil
|
||||
)?.takeRetainedValue(),
|
||||
let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue()
|
||||
else {
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
return mimeType as String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,41 +96,15 @@ extension LocalRepositoryShareOperations on LocalRepository {
|
||||
);
|
||||
}
|
||||
|
||||
bool createdProfile = false;
|
||||
final List<PersonProfile> nextPeople = current.people.toList(
|
||||
growable: true,
|
||||
);
|
||||
if (resolvedPerson == null) {
|
||||
final String? inferredName = _deriveAutoProfileName(
|
||||
sourceDisplayName: sourceDisplayName,
|
||||
sourceUserId: sourceUserId,
|
||||
sourceThreadId: sourceThreadId,
|
||||
return _queueSharedInbox(
|
||||
current: current,
|
||||
payload: payload,
|
||||
sourceFingerprint: sourceFingerprint,
|
||||
normalizedDisplayName: normalizedDisplayName,
|
||||
reason: SharedInboxReason.missingIdentity,
|
||||
candidateProfileIds: const <String>[],
|
||||
);
|
||||
if (inferredName == null) {
|
||||
return _queueSharedInbox(
|
||||
current: current,
|
||||
payload: payload,
|
||||
sourceFingerprint: sourceFingerprint,
|
||||
normalizedDisplayName: normalizedDisplayName,
|
||||
reason: SharedInboxReason.missingIdentity,
|
||||
candidateProfileIds: const <String>[],
|
||||
);
|
||||
}
|
||||
createdProfile = true;
|
||||
resolvedPerson = PersonProfile(
|
||||
id: 'p-${_uuid.v4()}',
|
||||
name: inferredName,
|
||||
relationship: 'WhatsApp Contact',
|
||||
affinityScore: 70,
|
||||
nextMoment: now.add(const Duration(days: 2)),
|
||||
tags: const <String>['whatsapp'],
|
||||
notes: 'Auto-created from shared message.',
|
||||
aliases: sourceDisplayName == null || sourceDisplayName == inferredName
|
||||
? const <String>[]
|
||||
: <String>[sourceDisplayName],
|
||||
lastUpdatedAt: now,
|
||||
);
|
||||
nextPeople.insert(0, resolvedPerson);
|
||||
}
|
||||
|
||||
return _ingestIntoResolvedProfile(
|
||||
@@ -141,8 +115,8 @@ extension LocalRepositoryShareOperations on LocalRepository {
|
||||
normalizedDisplayName: normalizedDisplayName,
|
||||
resolvedPerson: resolvedPerson,
|
||||
matchedLink: matchedLink,
|
||||
createdProfile: createdProfile,
|
||||
people: nextPeople,
|
||||
createdProfile: false,
|
||||
people: current.people,
|
||||
resolvedAutomatically: true,
|
||||
draft: _defaultDraftForPayload(payload),
|
||||
);
|
||||
@@ -298,14 +272,10 @@ extension LocalRepositoryShareOperations on LocalRepository {
|
||||
}) async {
|
||||
final LocalDataState current = _requireState();
|
||||
final DateTime now = DateTime.now();
|
||||
final String profileName =
|
||||
_trimToNull(name) ??
|
||||
_deriveAutoProfileName(
|
||||
sourceDisplayName: payload.sourceDisplayName,
|
||||
sourceUserId: payload.sourceUserId,
|
||||
sourceThreadId: payload.sourceThreadId,
|
||||
) ??
|
||||
'New Contact';
|
||||
final String? profileName = _trimToNull(name);
|
||||
if (profileName == null) {
|
||||
throw ArgumentError('A person name is required to create from a share');
|
||||
}
|
||||
final String normalizedRelationship = relationship.trim().isEmpty
|
||||
? 'Contact'
|
||||
: relationship.trim();
|
||||
@@ -842,16 +812,6 @@ SharedInboxEntry _requireSharedInboxEntry(
|
||||
throw ArgumentError('Shared inbox entry not found');
|
||||
}
|
||||
|
||||
String? _deriveAutoProfileName({
|
||||
required String? sourceDisplayName,
|
||||
required String? sourceUserId,
|
||||
required String? sourceThreadId,
|
||||
}) {
|
||||
return _trimToNull(sourceDisplayName) ??
|
||||
_trimToNull(sourceUserId) ??
|
||||
_trimToNull(sourceThreadId);
|
||||
}
|
||||
|
||||
String? _buildStableSourceFingerprint({
|
||||
required String sourceApp,
|
||||
required String? sourceUserId,
|
||||
|
||||
@@ -2,14 +2,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
/// Typed shell destinations used by the app navigation shell.
|
||||
enum AppDestination {
|
||||
dashboard('Dashboard', Icons.dashboard_rounded),
|
||||
shareInbox('Capture', Icons.inbox_rounded),
|
||||
people('People', Icons.people_alt_rounded),
|
||||
moments('Moments', Icons.auto_awesome_rounded),
|
||||
signals('Signals', Icons.tips_and_updates_rounded),
|
||||
aiReview('AI Review', Icons.rate_review_rounded),
|
||||
ideas('Ideas', Icons.lightbulb_outline_rounded),
|
||||
reminders('Reminders', Icons.notifications_active_outlined),
|
||||
sync('Sync', Icons.sync_rounded),
|
||||
aiReview('Digest', Icons.rate_review_rounded),
|
||||
settings('Settings', Icons.settings_rounded);
|
||||
|
||||
const AppDestination(this.label, this.icon);
|
||||
@@ -17,6 +12,3 @@ enum AppDestination {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
/// Secondary destinations exposed from the compact shell overflow menu.
|
||||
enum CompactSecondaryDestination { aiReview, ideas, reminders, sync, settings }
|
||||
|
||||
@@ -3,14 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/app/navigation/app_destination.dart';
|
||||
import 'package:relationship_saver/core/config/app_theme.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart';
|
||||
import 'package:relationship_saver/features/dashboard/presentation/dashboard_view.dart';
|
||||
import 'package:relationship_saver/features/ideas/presentation/ideas_view.dart';
|
||||
import 'package:relationship_saver/features/moments/presentation/moments_view.dart';
|
||||
import 'package:relationship_saver/features/people/presentation/people_view.dart';
|
||||
import 'package:relationship_saver/features/reminders/presentation/reminders_view.dart';
|
||||
import 'package:relationship_saver/features/settings/presentation/settings_view.dart';
|
||||
import 'package:relationship_saver/features/signals/presentation/signals_view.dart';
|
||||
import 'package:relationship_saver/features/sync/presentation/sync_view.dart';
|
||||
import 'package:relationship_saver/features/share_intake/presentation/share_inbox_view.dart';
|
||||
|
||||
/// Responsive shell that hosts the main feature slices.
|
||||
class AppShell extends ConsumerStatefulWidget {
|
||||
@@ -21,16 +16,11 @@ class AppShell extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AppShellState extends ConsumerState<AppShell> {
|
||||
AppDestination _destination = AppDestination.dashboard;
|
||||
AppDestination _destination = AppDestination.shareInbox;
|
||||
|
||||
static const List<AppDestination> _destinations = AppDestination.values;
|
||||
static const List<AppDestination> _compactPrimaryDestinations =
|
||||
<AppDestination>[
|
||||
AppDestination.dashboard,
|
||||
AppDestination.people,
|
||||
AppDestination.moments,
|
||||
AppDestination.signals,
|
||||
];
|
||||
AppDestination.values;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -65,6 +55,7 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
layoutBuilder: _expandedSwitcherLayout,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<AppDestination>(_destination),
|
||||
child: _screenFor(_destination),
|
||||
@@ -84,31 +75,10 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
final int compactIndex = _compactSelectedIndex(_destination);
|
||||
final AppDestination active = _compactPrimaryDestinations[compactIndex];
|
||||
return _CompactShell(
|
||||
title: active.label,
|
||||
selected: active,
|
||||
body: _screenFor(active),
|
||||
destinations: _compactPrimaryDestinations,
|
||||
onChange: _selectDestination,
|
||||
onOpenSecondary: (CompactSecondaryDestination destination) {
|
||||
final AppDestination screenDestination = switch (destination) {
|
||||
CompactSecondaryDestination.ideas => AppDestination.ideas,
|
||||
CompactSecondaryDestination.aiReview => AppDestination.aiReview,
|
||||
CompactSecondaryDestination.reminders => AppDestination.reminders,
|
||||
CompactSecondaryDestination.sync => AppDestination.sync,
|
||||
CompactSecondaryDestination.settings => AppDestination.settings,
|
||||
};
|
||||
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return _CompactSecondaryScreen(
|
||||
title: screenDestination.label,
|
||||
child: _screenFor(screenDestination),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,22 +99,12 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
|
||||
Widget _screenFor(AppDestination destination) {
|
||||
switch (destination) {
|
||||
case AppDestination.dashboard:
|
||||
return const DashboardView();
|
||||
case AppDestination.shareInbox:
|
||||
return const ShareInboxView();
|
||||
case AppDestination.people:
|
||||
return const PeopleView();
|
||||
case AppDestination.moments:
|
||||
return const MomentsView();
|
||||
case AppDestination.signals:
|
||||
return const SignalsView();
|
||||
case AppDestination.aiReview:
|
||||
return const AiDigestReviewView();
|
||||
case AppDestination.ideas:
|
||||
return const IdeasView();
|
||||
case AppDestination.reminders:
|
||||
return const RemindersView();
|
||||
case AppDestination.sync:
|
||||
return const SyncView();
|
||||
case AppDestination.settings:
|
||||
return const SettingsView();
|
||||
}
|
||||
@@ -255,7 +215,7 @@ class _Sidebar extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Tip: Use Signals daily and convert at least one into a concrete plan.',
|
||||
'Share messages or paste text into Capture. Nothing is assigned until you choose a person.',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
|
||||
@@ -269,69 +229,25 @@ class _Sidebar extends StatelessWidget {
|
||||
|
||||
class _CompactShell extends StatelessWidget {
|
||||
const _CompactShell({
|
||||
required this.title,
|
||||
required this.selected,
|
||||
required this.body,
|
||||
required this.destinations,
|
||||
required this.onChange,
|
||||
required this.onOpenSecondary,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final AppDestination selected;
|
||||
final Widget body;
|
||||
final List<AppDestination> destinations;
|
||||
final ValueChanged<AppDestination> onChange;
|
||||
final ValueChanged<CompactSecondaryDestination> onOpenSecondary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
PopupMenuButton<CompactSecondaryDestination>(
|
||||
tooltip: 'More',
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
onSelected: onOpenSecondary,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<CompactSecondaryDestination>>[
|
||||
const PopupMenuItem<CompactSecondaryDestination>(
|
||||
value: CompactSecondaryDestination.aiReview,
|
||||
child: Text('AI Review'),
|
||||
),
|
||||
const PopupMenuItem<CompactSecondaryDestination>(
|
||||
value: CompactSecondaryDestination.ideas,
|
||||
child: Text('Ideas'),
|
||||
),
|
||||
const PopupMenuItem<CompactSecondaryDestination>(
|
||||
value: CompactSecondaryDestination.reminders,
|
||||
child: Text('Reminders'),
|
||||
),
|
||||
const PopupMenuItem<CompactSecondaryDestination>(
|
||||
value: CompactSecondaryDestination.sync,
|
||||
child: Text('Sync'),
|
||||
),
|
||||
const PopupMenuItem<CompactSecondaryDestination>(
|
||||
value: CompactSecondaryDestination.settings,
|
||||
child: Text('Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
layoutBuilder: _expandedSwitcherLayout,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<AppDestination>(selected),
|
||||
child: body,
|
||||
@@ -355,27 +271,12 @@ class _CompactShell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CompactSecondaryScreen extends StatelessWidget {
|
||||
const _CompactSecondaryScreen({required this.title, required this.child});
|
||||
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: <Color>[Color(0xFFF3F8FB), Color(0xFFEAF1F8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _expandedSwitcherLayout(
|
||||
Widget? currentChild,
|
||||
List<Widget> previousChildren,
|
||||
) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[...previousChildren, ?currentChild],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,162 +219,15 @@ class LocalDataState {
|
||||
);
|
||||
}
|
||||
|
||||
static LocalDataState seed() {
|
||||
return LocalDataState(
|
||||
people: <PersonProfile>[
|
||||
PersonProfile(
|
||||
id: 'p-ava',
|
||||
name: 'Ava Hart',
|
||||
relationship: 'Partner',
|
||||
affinityScore: 92,
|
||||
nextMoment: DateTime(2026, 2, 16, 19, 30),
|
||||
tags: <String>['coffee', 'books', 'slow mornings'],
|
||||
notes: 'Prefers thoughtful plans over expensive plans.',
|
||||
aliases: <String>['Aves'],
|
||||
location: 'Austin, TX',
|
||||
lastUpdatedAt: DateTime(2026, 2, 14, 9, 0),
|
||||
lastInteractedAt: DateTime(2026, 2, 12, 18, 30),
|
||||
),
|
||||
PersonProfile(
|
||||
id: 'p-jordan',
|
||||
name: 'Jordan Lee',
|
||||
relationship: 'Close Friend',
|
||||
affinityScore: 78,
|
||||
nextMoment: DateTime(2026, 2, 18, 18, 0),
|
||||
tags: <String>['running', 'street food'],
|
||||
notes: 'Has a race on Sunday; ask how training is going.',
|
||||
location: 'Chicago, IL',
|
||||
lastUpdatedAt: DateTime(2026, 2, 13, 8, 30),
|
||||
lastInteractedAt: DateTime(2026, 2, 11, 19, 0),
|
||||
),
|
||||
PersonProfile(
|
||||
id: 'p-mila',
|
||||
name: 'Mila Stone',
|
||||
relationship: 'Sister',
|
||||
affinityScore: 84,
|
||||
nextMoment: DateTime(2026, 2, 20, 12, 0),
|
||||
tags: <String>['plants', 'retro music'],
|
||||
notes: 'Birthday prep should start this week.',
|
||||
aliases: <String>['Millie'],
|
||||
location: 'Seattle, WA',
|
||||
lastUpdatedAt: DateTime(2026, 2, 13, 14, 20),
|
||||
lastInteractedAt: DateTime(2026, 2, 12, 14, 20),
|
||||
),
|
||||
],
|
||||
moments: <RelationshipMoment>[
|
||||
RelationshipMoment(
|
||||
id: 'm-1',
|
||||
personId: 'p-ava',
|
||||
title: 'Sunday Walk Tradition',
|
||||
summary: 'Short walk + no-phone hour worked really well.',
|
||||
at: DateTime(2026, 2, 9, 9, 30),
|
||||
type: 'ritual',
|
||||
),
|
||||
RelationshipMoment(
|
||||
id: 'm-2',
|
||||
personId: 'p-jordan',
|
||||
title: 'Post-workout Check-in',
|
||||
summary: 'Shared training playlist and grabbed smoothies.',
|
||||
at: DateTime(2026, 2, 11, 19, 0),
|
||||
type: 'support',
|
||||
),
|
||||
RelationshipMoment(
|
||||
id: 'm-3',
|
||||
personId: 'p-mila',
|
||||
title: 'Gift Idea Captured',
|
||||
summary: 'Found a vintage lamp from her saved style board.',
|
||||
at: DateTime(2026, 2, 12, 14, 20),
|
||||
type: 'gift',
|
||||
),
|
||||
],
|
||||
ideas: <RelationshipIdea>[
|
||||
RelationshipIdea(
|
||||
id: 'i-1',
|
||||
personId: 'p-ava',
|
||||
type: IdeaType.gift,
|
||||
title: 'Handwritten recipe journal',
|
||||
details: 'Collect 10 memories and recipes from this year.',
|
||||
createdAt: DateTime(2026, 2, 12, 9),
|
||||
),
|
||||
RelationshipIdea(
|
||||
id: 'i-2',
|
||||
personId: 'p-mila',
|
||||
type: IdeaType.event,
|
||||
title: 'Plant market + brunch date',
|
||||
details: 'Saturday morning slot; book nearby cafe in advance.',
|
||||
createdAt: DateTime(2026, 2, 13, 11),
|
||||
),
|
||||
],
|
||||
reminders: <ReminderRule>[
|
||||
ReminderRule(
|
||||
id: 'r-1',
|
||||
personId: 'p-jordan',
|
||||
title: 'Weekly check-in message',
|
||||
cadence: ReminderCadence.weekly,
|
||||
nextAt: DateTime(2026, 2, 18, 18),
|
||||
),
|
||||
ReminderRule(
|
||||
id: 'r-2',
|
||||
personId: 'p-ava',
|
||||
title: 'Sunday ritual planning',
|
||||
cadence: ReminderCadence.weekly,
|
||||
nextAt: DateTime(2026, 2, 16, 10),
|
||||
),
|
||||
],
|
||||
tasks: <DashboardTask>[
|
||||
DashboardTask(
|
||||
id: 't-1',
|
||||
title: 'Plan Friday dinner with Ava',
|
||||
description: 'Keep it low-key: ramen + bookstore stop.',
|
||||
when: DateTime(2026, 2, 16, 17, 0),
|
||||
),
|
||||
DashboardTask(
|
||||
id: 't-2',
|
||||
title: 'Send race-day encouragement to Jordan',
|
||||
description: 'Voice note before 8:00 AM.',
|
||||
when: DateTime(2026, 2, 17, 7, 30),
|
||||
),
|
||||
DashboardTask(
|
||||
id: 't-3',
|
||||
title: 'Finalize Mila birthday shortlist',
|
||||
description: 'Pick 1 meaningful gift and 1 backup.',
|
||||
when: DateTime(2026, 2, 18, 20, 0),
|
||||
),
|
||||
],
|
||||
personFacts: <PersonFact>[
|
||||
PersonFact(
|
||||
id: 'pf-1',
|
||||
personId: 'p-ava',
|
||||
type: CapturedFactType.like,
|
||||
text: 'Quiet brunch spots and recipe-book stores',
|
||||
label: 'Weekend preference',
|
||||
sourceKind: CaptureSourceKind.manual,
|
||||
createdAt: DateTime(2026, 2, 10, 8, 0),
|
||||
updatedAt: DateTime(2026, 2, 10, 8, 0),
|
||||
),
|
||||
PersonFact(
|
||||
id: 'pf-2',
|
||||
personId: 'p-mila',
|
||||
type: CapturedFactType.giftIdea,
|
||||
text: 'Vintage ceramic planter in muted green',
|
||||
label: 'Gift lead',
|
||||
sourceKind: CaptureSourceKind.manual,
|
||||
createdAt: DateTime(2026, 2, 12, 14, 25),
|
||||
updatedAt: DateTime(2026, 2, 12, 14, 25),
|
||||
),
|
||||
],
|
||||
importantDates: <PersonImportantDate>[
|
||||
PersonImportantDate(
|
||||
id: 'pd-1',
|
||||
personId: 'p-mila',
|
||||
label: 'Birthday',
|
||||
date: DateTime(2026, 3, 3),
|
||||
classification: 'birthday',
|
||||
sourceKind: CaptureSourceKind.manual,
|
||||
createdAt: DateTime(2026, 2, 12, 14, 30),
|
||||
updatedAt: DateTime(2026, 2, 12, 14, 30),
|
||||
),
|
||||
],
|
||||
static LocalDataState empty() {
|
||||
return const LocalDataState(
|
||||
people: <PersonProfile>[],
|
||||
moments: <RelationshipMoment>[],
|
||||
ideas: <RelationshipIdea>[],
|
||||
reminders: <ReminderRule>[],
|
||||
tasks: <DashboardTask>[],
|
||||
);
|
||||
}
|
||||
|
||||
static LocalDataState seed() => empty();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class AppConfig {
|
||||
|
||||
/// Enables periodic startup/resume sync triggers while authenticated.
|
||||
static bool get enableBackgroundSync =>
|
||||
const bool.fromEnvironment('ENABLE_BACKGROUND_SYNC', defaultValue: true);
|
||||
const bool.fromEnvironment('ENABLE_BACKGROUND_SYNC', defaultValue: false);
|
||||
|
||||
/// Enables local notifications runtime for reminder delivery.
|
||||
static bool get enableLocalNotifications => const bool.fromEnvironment(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:relationship_saver/app/state/local_data_state.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:relationship_saver/features/people/domain/person_models.dart';
|
||||
|
||||
class AnonymizedLlmDigestContext {
|
||||
@@ -22,11 +23,13 @@ class AnonymizedLlmContextBuilder {
|
||||
const AnonymizedLlmContextBuilder({
|
||||
this.maxPeople = 20,
|
||||
this.maxSignalsPerPerson = 8,
|
||||
this.maxPriorSuggestionsPerPerson = 6,
|
||||
DateTime Function()? now,
|
||||
}) : _now = now ?? DateTime.now;
|
||||
|
||||
final int maxPeople;
|
||||
final int maxSignalsPerPerson;
|
||||
final int maxPriorSuggestionsPerPerson;
|
||||
final DateTime Function() _now;
|
||||
|
||||
AnonymizedLlmDigestContext build(LocalDataState state) {
|
||||
@@ -66,6 +69,7 @@ class AnonymizedLlmContextBuilder {
|
||||
'Do not infer or ask for names.',
|
||||
'Return JSON only.',
|
||||
'Prefer practical suggestions that can be reviewed before saving.',
|
||||
'Do not repeat priorSuggestions for the same personToken.',
|
||||
],
|
||||
'limits': <String, dynamic>{
|
||||
'maxItems': 10,
|
||||
@@ -184,9 +188,27 @@ class AnonymizedLlmContextBuilder {
|
||||
.map((PersonPreferenceSignal signal) => 'avoid ${signal.label}')
|
||||
.toList(growable: false),
|
||||
).take(maxSignalsPerPerson).toList(growable: false),
|
||||
'priorSuggestions': _priorSuggestions(state, person),
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, String>> _priorSuggestions(
|
||||
LocalDataState state,
|
||||
PersonProfile person,
|
||||
) {
|
||||
return state.aiSuggestionDrafts
|
||||
.where((AiSuggestionDraft draft) => draft.personId == person.id)
|
||||
.take(maxPriorSuggestionsPerPerson)
|
||||
.map(
|
||||
(AiSuggestionDraft draft) => <String, String>{
|
||||
'kind': draft.kind.name,
|
||||
'status': draft.status.name,
|
||||
'title': _safeCategory(draft.title),
|
||||
},
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _relationshipCategory(String value) {
|
||||
final String normalized = value.toLowerCase();
|
||||
if (_containsAny(normalized, <String>[
|
||||
|
||||
@@ -391,11 +391,11 @@ class _PersonDetail extends ConsumerWidget {
|
||||
]
|
||||
: sharedMessages
|
||||
.take(compact ? 3 : 5)
|
||||
.map(
|
||||
.map<Widget>(
|
||||
(SharedMessageEntry entry) => _PeopleInsightRow(
|
||||
title:
|
||||
entry.sourceDisplayName?.trim().isNotEmpty == true
|
||||
? entry.sourceDisplayName!.trim()
|
||||
? 'Detected in text: ${entry.sourceDisplayName!.trim()}'
|
||||
: 'Shared from ${entry.sourceApp}',
|
||||
subtitle: entry.messageText.trim().isEmpty
|
||||
? 'Imported message content is empty'
|
||||
@@ -404,6 +404,23 @@ class _PersonDetail extends ConsumerWidget {
|
||||
'${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'} • ${_shortDateTimeLabel(entry.sharedAt)}',
|
||||
),
|
||||
)
|
||||
.followedBy(<Widget>[
|
||||
if (sharedMessages.length > (compact ? 3 : 5))
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => _showSharedMessageHistory(
|
||||
context,
|
||||
person: person,
|
||||
sharedMessages: sharedMessages,
|
||||
),
|
||||
icon: const Icon(Icons.list_alt_rounded),
|
||||
label: Text(
|
||||
'View all ${sharedMessages.length} shared texts',
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
.toList(growable: false),
|
||||
),
|
||||
],
|
||||
@@ -411,6 +428,70 @@ class _PersonDetail extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSharedMessageHistory(
|
||||
BuildContext context, {
|
||||
required PersonProfile person,
|
||||
required List<SharedMessageEntry> sharedMessages,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.82,
|
||||
minChildSize: 0.45,
|
||||
maxChildSize: 0.95,
|
||||
builder: (BuildContext context, ScrollController scrollController) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Shared texts',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${person.name} • ${sharedMessages.length} item${sharedMessages.length == 1 ? '' : 's'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
controller: scrollController,
|
||||
itemCount: sharedMessages.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final SharedMessageEntry entry = sharedMessages[index];
|
||||
return _PeopleInsightRow(
|
||||
title:
|
||||
entry.sourceDisplayName?.trim().isNotEmpty == true
|
||||
? 'Detected in text: ${entry.sourceDisplayName!.trim()}'
|
||||
: 'Shared from ${entry.sourceApp}',
|
||||
subtitle: entry.messageText.trim().isEmpty
|
||||
? entry.url ?? 'Imported message content is empty'
|
||||
: entry.messageText.trim(),
|
||||
meta:
|
||||
'${entry.sourceApp.toUpperCase()} • ${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'} • ${_shortDateTimeLabel(entry.sharedAt)}',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPeoplePreferenceEvidence(
|
||||
BuildContext context, {
|
||||
required PersonPreferenceSignal signal,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_rev
|
||||
import 'package:relationship_saver/features/auth/session_controller.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
|
||||
import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_flow.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_inbox_view.dart';
|
||||
@@ -210,6 +211,11 @@ class SettingsView extends ConsumerWidget {
|
||||
icon: const Icon(Icons.inbox_rounded),
|
||||
label: const Text('Open Share Inbox'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showShareDiagnostics(context),
|
||||
icon: const Icon(Icons.bug_report_rounded),
|
||||
label: const Text('Share Diagnostics'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -411,6 +417,26 @@ class SettingsView extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showShareDiagnostics(BuildContext context) async {
|
||||
final ShareIntakeDebugLog debugLog = const ShareIntakeDebugLog();
|
||||
final List<ShareIntakeDebugEvent> events = await debugLog.read();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _ShareDiagnosticsDialog(
|
||||
events: events,
|
||||
onClear: () async {
|
||||
await debugLog.clear();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingRow extends StatelessWidget {
|
||||
@@ -462,6 +488,57 @@ class _SettingRow extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareDiagnosticsDialog extends StatelessWidget {
|
||||
const _ShareDiagnosticsDialog({required this.events, required this.onClear});
|
||||
|
||||
final List<ShareIntakeDebugEvent> events;
|
||||
final Future<void> Function() onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Share Diagnostics'),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
child: events.isEmpty
|
||||
? const Text(
|
||||
'No Flutter share events recorded yet. If you tried sharing, the native share extension likely did not open the app or the plugin did not receive the URL callback.',
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: events
|
||||
.map(
|
||||
(ShareIntakeDebugEvent event) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SelectableText(
|
||||
'${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}',
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: events.isEmpty ? null : onClear,
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _debugTimeLabel(DateTime value) {
|
||||
return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
class _LlmConfigDraft {
|
||||
const _LlmConfigDraft({
|
||||
required this.provider,
|
||||
|
||||
@@ -39,12 +39,7 @@ Future<SharedMessageIngestResult?> startShareCaptureReview(
|
||||
required WidgetRef ref,
|
||||
required SharedPayload payload,
|
||||
}) {
|
||||
return showShareCaptureReviewSheet(
|
||||
context,
|
||||
ref: ref,
|
||||
payload: payload,
|
||||
initialPersonId: ref.read(selectedPersonIdProvider),
|
||||
);
|
||||
return showShareCaptureReviewSheet(context, ref: ref, payload: payload);
|
||||
}
|
||||
|
||||
void openShareCaptureDestination(
|
||||
@@ -73,10 +68,10 @@ void openShareCaptureDestination(
|
||||
|
||||
String shareCaptureResultMessage(SharedMessageIngestResult result) {
|
||||
if (result.isQueuedForResolution) {
|
||||
return 'Saved shared capture to inbox.';
|
||||
return 'Saved shared text to Capture inbox.';
|
||||
}
|
||||
if (result.createdProfile) {
|
||||
return 'Saved shared capture and created ${result.profileName}.';
|
||||
return 'Saved shared text and created ${result.profileName}.';
|
||||
}
|
||||
return 'Saved shared capture to ${result.profileName}.';
|
||||
return 'Saved shared text to ${result.profileName}.';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1';
|
||||
const int _maxShareIntakeDebugEvents = 40;
|
||||
|
||||
class ShareIntakeDebugEvent {
|
||||
const ShareIntakeDebugEvent({
|
||||
required this.at,
|
||||
required this.stage,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
factory ShareIntakeDebugEvent.fromJson(Map<String, dynamic> json) {
|
||||
return ShareIntakeDebugEvent(
|
||||
at: DateTime.parse(
|
||||
json['at'] as String? ?? DateTime.now().toUtc().toIso8601String(),
|
||||
).toLocal(),
|
||||
stage: json['stage'] as String? ?? 'unknown',
|
||||
message: json['message'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime at;
|
||||
final String stage;
|
||||
final String message;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'at': at.toUtc().toIso8601String(),
|
||||
'stage': stage,
|
||||
'message': message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ShareIntakeDebugLog {
|
||||
const ShareIntakeDebugLog();
|
||||
|
||||
Future<List<ShareIntakeDebugEvent>> read() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? raw = prefs.getString(_shareIntakeDebugLogKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return const <ShareIntakeDebugEvent>[];
|
||||
}
|
||||
try {
|
||||
final List<dynamic> items = jsonDecode(raw) as List<dynamic>;
|
||||
return items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ShareIntakeDebugEvent.fromJson)
|
||||
.toList(growable: false);
|
||||
} on FormatException {
|
||||
return const <ShareIntakeDebugEvent>[];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> record(String stage, String message) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final List<ShareIntakeDebugEvent> current = await read();
|
||||
final List<ShareIntakeDebugEvent> next = <ShareIntakeDebugEvent>[
|
||||
ShareIntakeDebugEvent(at: DateTime.now(), stage: stage, message: message),
|
||||
...current,
|
||||
].take(_maxShareIntakeDebugEvents).toList(growable: false);
|
||||
await prefs.setString(
|
||||
_shareIntakeDebugLogKey,
|
||||
jsonEncode(
|
||||
next
|
||||
.map((ShareIntakeDebugEvent event) => event.toJson())
|
||||
.toList(growable: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_shareIntakeDebugLogKey);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:relationship_saver/core/config/app_config.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_flow.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ class IncomingShareListener extends ConsumerStatefulWidget {
|
||||
class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
|
||||
StreamSubscription<List<SharedMediaFile>>? _mediaSubscription;
|
||||
String? _lastToken;
|
||||
final ShareIntakeDebugLog _debugLog = const ShareIntakeDebugLog();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -41,25 +43,44 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
|
||||
|
||||
Future<void> _start() async {
|
||||
if (!AppConfig.enableWhatsAppShareIntake || kIsWeb) {
|
||||
await _debugLog.record(
|
||||
'listener_skipped',
|
||||
'enabled=${AppConfig.enableWhatsAppShareIntake}, kIsWeb=$kIsWeb',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _debugLog.record('listener_start', 'Subscribing to share intents.');
|
||||
_mediaSubscription = ReceiveSharingIntent.instance
|
||||
.getMediaStream()
|
||||
.listen(
|
||||
(List<SharedMediaFile> files) =>
|
||||
_handleMediaList(files, autoOpenDestination: false),
|
||||
onError: (_) {},
|
||||
(List<SharedMediaFile> files) {
|
||||
unawaited(
|
||||
_debugLog.record(
|
||||
'media_stream',
|
||||
'Received ${files.length} shared item(s).',
|
||||
),
|
||||
);
|
||||
unawaited(_handleMediaList(files, autoOpenDestination: false));
|
||||
},
|
||||
onError: (Object error) {
|
||||
unawaited(_debugLog.record('media_stream_error', '$error'));
|
||||
},
|
||||
);
|
||||
|
||||
final List<SharedMediaFile> initialMedia = await ReceiveSharingIntent
|
||||
.instance
|
||||
.getInitialMedia();
|
||||
await _debugLog.record(
|
||||
'initial_media',
|
||||
'Received ${initialMedia.length} initial shared item(s).',
|
||||
);
|
||||
if (initialMedia.isNotEmpty) {
|
||||
await _handleMediaList(initialMedia, autoOpenDestination: true);
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
await _debugLog.record('listener_error', '$error');
|
||||
// Unsupported platform/plugin state should not break app usage.
|
||||
}
|
||||
}
|
||||
@@ -69,8 +90,16 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
|
||||
required bool autoOpenDestination,
|
||||
}) async {
|
||||
for (final SharedMediaFile file in files) {
|
||||
await _debugLog.record(
|
||||
'media_item',
|
||||
'type=${file.type.name}, mime=${file.mimeType ?? 'none'}, pathLength=${file.path.length}, messageLength=${file.message?.length ?? 0}',
|
||||
);
|
||||
final String? rawText = _extractText(file);
|
||||
if (rawText == null) {
|
||||
await _debugLog.record(
|
||||
'extract_skip',
|
||||
'No usable text for type=${file.type.name}.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await _handleRawText(
|
||||
@@ -129,12 +158,21 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
|
||||
sourceAppHint: sourceAppHint,
|
||||
);
|
||||
if (payload == null) {
|
||||
await _debugLog.record(
|
||||
'payload_skip',
|
||||
'Parser returned null for rawLength=${trimmed.length}.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _debugLog.record(
|
||||
'payload_parsed',
|
||||
'source=${payload.sourceApp}, type=${payload.payloadType.name}, rawLength=${payload.rawText.length}, hasUrl=${payload.url != null}, detectedSender=${payload.sourceDisplayName != null}',
|
||||
);
|
||||
|
||||
final String token =
|
||||
'${payload.sourceApp}:${payload.sourceUserId ?? payload.sourceDisplayName ?? ''}:${payload.rawText}:${payload.url ?? ''}';
|
||||
if (_lastToken == token) {
|
||||
await _debugLog.record('payload_duplicate', 'Ignored duplicate share.');
|
||||
return;
|
||||
}
|
||||
_lastToken = token;
|
||||
@@ -149,8 +187,17 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
|
||||
);
|
||||
|
||||
if (!mounted || result == null) {
|
||||
await _debugLog.record('review_cancelled', 'Share review closed.');
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
_debugLog.record(
|
||||
'review_result',
|
||||
result.isQueuedForResolution
|
||||
? 'Queued for inbox.'
|
||||
: 'Saved to profile; createdProfile=${result.createdProfile}.',
|
||||
),
|
||||
);
|
||||
|
||||
if (autoOpenDestination) {
|
||||
openShareCaptureDestination(context, ref: ref, result: result);
|
||||
|
||||
@@ -88,12 +88,12 @@ class _ShareCaptureReviewSheetState
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Save Shared Capture',
|
||||
'Save Shared Text',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Review the incoming text or link, then assign it safely.',
|
||||
'Choose who this belongs to, or keep it in the inbox for later review.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
@@ -101,10 +101,7 @@ class _ShareCaptureReviewSheetState
|
||||
const SizedBox(height: 14),
|
||||
_PayloadPreview(payload: widget.payload),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'What kind of info is this?',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text('Type', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -344,9 +341,7 @@ class _ShareCaptureReviewSheetState
|
||||
await showDialog<_CreatePersonFromShareDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _CreatePersonFromShareDialog(
|
||||
initialName:
|
||||
widget.payload.sourceDisplayName ??
|
||||
widget.payload.rawText.split(RegExp(r'\s+')).take(2).join(' '),
|
||||
detectedName: widget.payload.sourceDisplayName,
|
||||
),
|
||||
);
|
||||
if (draft == null) {
|
||||
@@ -445,7 +440,7 @@ class _PayloadPreview extends StatelessWidget {
|
||||
if (payload.sourceDisplayName != null) ...<Widget>[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Sender: ${payload.sourceDisplayName}',
|
||||
'Detected in text: ${payload.sourceDisplayName}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
|
||||
@@ -484,9 +479,9 @@ class _CreatePersonFromShareDraft {
|
||||
}
|
||||
|
||||
class _CreatePersonFromShareDialog extends StatefulWidget {
|
||||
const _CreatePersonFromShareDialog({required this.initialName});
|
||||
const _CreatePersonFromShareDialog({this.detectedName});
|
||||
|
||||
final String initialName;
|
||||
final String? detectedName;
|
||||
|
||||
@override
|
||||
State<_CreatePersonFromShareDialog> createState() =>
|
||||
@@ -504,7 +499,7 @@ class _CreatePersonFromShareDialogState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.initialName);
|
||||
_nameController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -523,9 +518,21 @@ class _CreatePersonFromShareDialogState
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.detectedName?.trim().isNotEmpty == true) ...<Widget>[
|
||||
Text(
|
||||
'Detected message author: ${widget.detectedName!.trim()}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Person name',
|
||||
hintText: 'Enter the real contact',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:relationship_saver/core/config/app_theme.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/local/local_repository.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_review_sheet.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_payload_parser.dart';
|
||||
import 'package:relationship_saver/features/shared/frosted_card.dart';
|
||||
|
||||
/// Review unresolved shared messages and map them to a person profile.
|
||||
@@ -45,13 +47,25 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Share Inbox',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 10,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Capture',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _addTextCapture,
|
||||
icon: const Icon(Icons.edit_note_rounded),
|
||||
label: const Text('Add Text'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Resolve shared messages that could not be matched confidently.',
|
||||
'Share messages here, paste copied text, then choose or create the right person.',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
@@ -59,7 +73,9 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
|
||||
const SizedBox(height: 14),
|
||||
Expanded(
|
||||
child: localState.sharedInbox.isEmpty
|
||||
? _EmptyInbox(compact: compact)
|
||||
? SingleChildScrollView(
|
||||
child: _EmptyInbox(compact: compact),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: localState.sharedInbox.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
@@ -178,13 +194,8 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
|
||||
}
|
||||
final _CreateProfileDraft? draft = await showDialog<_CreateProfileDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _CreateProfileDialog(
|
||||
initialName:
|
||||
entry.sourceDisplayName ??
|
||||
entry.sourceUserId ??
|
||||
entry.sourceThreadId ??
|
||||
'',
|
||||
),
|
||||
builder: (BuildContext context) =>
|
||||
_CreateProfileDialog(detectedName: entry.sourceDisplayName),
|
||||
);
|
||||
if (draft == null || !mounted) {
|
||||
return;
|
||||
@@ -232,6 +243,38 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _addTextCapture() async {
|
||||
if (_submitting) {
|
||||
return;
|
||||
}
|
||||
final String? rawText = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => const _AddTextDialog(),
|
||||
);
|
||||
if (rawText == null || rawText.trim().isEmpty || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: rawText,
|
||||
sourceApp: 'manual',
|
||||
platform: 'manual',
|
||||
createdAt: DateTime.now(),
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
final SharedMessageIngestResult? result = await showShareCaptureReviewSheet(
|
||||
context,
|
||||
ref: ref,
|
||||
payload: payload,
|
||||
);
|
||||
if (result == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(_captureResultMessage(result))));
|
||||
}
|
||||
|
||||
Future<void> _runRepositoryAction(Future<void> Function() action) async {
|
||||
setState(() {
|
||||
_submitting = true;
|
||||
@@ -453,6 +496,60 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddTextDialog extends StatefulWidget {
|
||||
const _AddTextDialog();
|
||||
|
||||
@override
|
||||
State<_AddTextDialog> createState() => _AddTextDialogState();
|
||||
}
|
||||
|
||||
class _AddTextDialogState extends State<_AddTextDialog> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Add Text'),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
minLines: 5,
|
||||
maxLines: 10,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Shared or copied text',
|
||||
hintText: 'Paste a message, note, link, preference, or date...',
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(_controller.text),
|
||||
child: const Text('Review'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CandidateReview {
|
||||
const _CandidateReview({
|
||||
required this.person,
|
||||
@@ -1073,6 +1170,16 @@ String _factTypeLabel(CapturedFactType type) {
|
||||
};
|
||||
}
|
||||
|
||||
String _captureResultMessage(SharedMessageIngestResult result) {
|
||||
if (result.isQueuedForResolution) {
|
||||
return 'Saved to Capture inbox.';
|
||||
}
|
||||
if (result.createdProfile) {
|
||||
return 'Saved and created ${result.profileName}.';
|
||||
}
|
||||
return 'Saved to ${result.profileName}.';
|
||||
}
|
||||
|
||||
class _EmptyInbox extends StatelessWidget {
|
||||
const _EmptyInbox({required this.compact});
|
||||
|
||||
@@ -1103,7 +1210,7 @@ class _EmptyInbox extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Incoming shared captures that need manual person mapping or fact triage will appear here.',
|
||||
'Share a message into the app or use Add Text to start building your private relationship notes.',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
|
||||
@@ -1132,9 +1239,9 @@ class _CreateProfileDraft {
|
||||
}
|
||||
|
||||
class _CreateProfileDialog extends StatefulWidget {
|
||||
const _CreateProfileDialog({required this.initialName});
|
||||
const _CreateProfileDialog({this.detectedName});
|
||||
|
||||
final String initialName;
|
||||
final String? detectedName;
|
||||
|
||||
@override
|
||||
State<_CreateProfileDialog> createState() => _CreateProfileDialogState();
|
||||
@@ -1149,7 +1256,7 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.initialName);
|
||||
_nameController = TextEditingController();
|
||||
_relationshipController = TextEditingController(text: 'Contact');
|
||||
_locationController = TextEditingController();
|
||||
_aliasesController = TextEditingController();
|
||||
@@ -1172,9 +1279,21 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.detectedName?.trim().isNotEmpty == true) ...<Widget>[
|
||||
Text(
|
||||
'Detected message author: ${widget.detectedName!.trim()}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Person name',
|
||||
hintText: 'Enter the real contact',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
|
||||
@@ -97,21 +97,9 @@ class BackendGatewayFake implements BackendGateway {
|
||||
|
||||
@override
|
||||
Future<SyncPullResult> pullChanges({String? cursor, int limit = 200}) async {
|
||||
final List<ChangeEnvelope> changes = <ChangeEnvelope>[
|
||||
ChangeEnvelope(
|
||||
schemaVersion: 1,
|
||||
entityType: 'person',
|
||||
entityId: 'person-1',
|
||||
op: ChangeOperation.upsert,
|
||||
modifiedAt: _seedTime,
|
||||
clientMutationId: 'cm-1',
|
||||
payload: const <String, dynamic>{'name': 'Alex', 'importance': 5},
|
||||
),
|
||||
];
|
||||
|
||||
return SyncPullResult(
|
||||
cursor: 'fake-pull-cursor-1',
|
||||
changes: changes.take(limit).toList(),
|
||||
cursor: cursor ?? 'fake-pull-cursor-empty',
|
||||
changes: const <ChangeEnvelope>[],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -65,10 +62,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
final int ideaCount = initial.ideas.length;
|
||||
|
||||
await container
|
||||
@@ -112,3 +109,19 @@ ProviderContainer _createContainer() {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _addTestPerson(ProviderContainer container) async {
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
return state.people.single.id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/app/state/local_data_state.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/application/anonymized_llm_context_builder.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:relationship_saver/features/dashboard/domain/dashboard_models.dart';
|
||||
import 'package:relationship_saver/features/ideas/domain/idea_models.dart';
|
||||
import 'package:relationship_saver/features/moments/domain/moment_models.dart';
|
||||
@@ -73,6 +74,19 @@ void main() {
|
||||
isSensitive: true,
|
||||
),
|
||||
],
|
||||
aiSuggestionDrafts: <AiSuggestionDraft>[
|
||||
AiSuggestionDraft(
|
||||
id: 'ai-1',
|
||||
personId: 'p-1',
|
||||
kind: AiSuggestionKind.eventIdea,
|
||||
title: 'Jazz concert downtown',
|
||||
details: 'Should not need to repeat the details.',
|
||||
suggestedAt: DateTime(2026, 5, 10),
|
||||
confidence: 0.8,
|
||||
status: AiSuggestionStatus.dismissed,
|
||||
sourceRunId: 'digest-old',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final AnonymizedLlmDigestContext context = AnonymizedLlmContextBuilder(
|
||||
@@ -90,5 +104,9 @@ void main() {
|
||||
expect(prompt, isNot(contains('thread-secret')));
|
||||
expect(prompt, isNot(contains('Raw private chat text')));
|
||||
expect(prompt, isNot(contains('Secret medical detail')));
|
||||
expect(prompt, contains('priorSuggestions'));
|
||||
expect(prompt, contains('jazz concert downtown'));
|
||||
expect(prompt, contains('dismissed'));
|
||||
expect(prompt, isNot(contains('Should not need to repeat the details.')));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>['tea'],
|
||||
);
|
||||
await container.read(llmDigestConfigProvider.notifier).setEnabled(true);
|
||||
|
||||
final LlmDigestRunResult result = await container
|
||||
|
||||
@@ -40,7 +40,7 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Sign In'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 450));
|
||||
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
expect(find.text('Capture'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('Settings').first);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 450));
|
||||
|
||||
@@ -47,7 +47,7 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Sign In'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
expect(find.text('Capture'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('People').last);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
@@ -91,13 +91,5 @@ void main() {
|
||||
}),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('More'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.tap(find.text('Sync').last);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Queue Status'), findsOneWidget);
|
||||
expect(find.text('Pending changes'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,13 +25,23 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Ava Hart',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState seeded = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = seeded.people.first.id;
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.upsertInferredPreferenceSignalObservation(
|
||||
personId: seeded.people.first.id,
|
||||
personId: personId,
|
||||
key: 'drink:tea',
|
||||
label: 'Tea',
|
||||
category: 'drink',
|
||||
@@ -58,7 +68,7 @@ void main() {
|
||||
expect(find.text('Explore'), findsOneWidget);
|
||||
|
||||
final Finder avaNode = find.byKey(
|
||||
const ValueKey<String>('graph-node-p-ava'),
|
||||
ValueKey<String>('graph-node-$personId'),
|
||||
);
|
||||
expect(avaNode, findsOneWidget);
|
||||
|
||||
|
||||
@@ -17,15 +17,17 @@ void main() {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
});
|
||||
|
||||
test('seeds local data on first load', () async {
|
||||
test('starts with empty local data on first load', () async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final state = await container.read(localRepositoryProvider.future);
|
||||
|
||||
expect(state.people, isNotEmpty);
|
||||
expect(state.moments, isNotEmpty);
|
||||
expect(state.tasks, isNotEmpty);
|
||||
expect(state.people, isEmpty);
|
||||
expect(state.moments, isEmpty);
|
||||
expect(state.ideas, isEmpty);
|
||||
expect(state.reminders, isEmpty);
|
||||
expect(state.tasks, isEmpty);
|
||||
});
|
||||
|
||||
test('adds and removes person with persistence state update', () async {
|
||||
@@ -62,8 +64,8 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final state = await container.read(localRepositoryProvider.future);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.moments.length;
|
||||
|
||||
await container
|
||||
@@ -86,10 +88,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
|
||||
final PersonPreferenceSignal first = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -161,7 +160,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'ingestSharedMessage auto-creates profile and reuses source link for follow-up messages',
|
||||
'ingestSharedMessage queues unknown identity, then reuses source link after manual creation',
|
||||
() async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
@@ -184,22 +183,35 @@ void main() {
|
||||
final LocalDataState afterFirst = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(first.createdProfile, isTrue);
|
||||
expect(afterFirst.people.length, before.people.length + 1);
|
||||
final SourceProfileLink createdLink = afterFirst.sourceLinks.firstWhere(
|
||||
(SourceProfileLink link) => link.profileId == first.profileId,
|
||||
expect(first.isQueuedForResolution, isTrue);
|
||||
expect(first.createdProfile, isFalse);
|
||||
expect(afterFirst.people.length, before.people.length);
|
||||
expect(
|
||||
afterFirst.sharedInbox.single.reason,
|
||||
SharedInboxReason.missingIdentity,
|
||||
);
|
||||
|
||||
final SharedMessageIngestResult created = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.resolveSharedInboxByCreatingProfile(
|
||||
inboxEntryId: first.inboxEntryId!,
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
);
|
||||
|
||||
final LocalDataState afterCreate = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(created.createdProfile, isTrue);
|
||||
expect(afterCreate.people.length, before.people.length + 1);
|
||||
final SourceProfileLink createdLink = afterCreate.sourceLinks.firstWhere(
|
||||
(SourceProfileLink link) => link.profileId == created.profileId,
|
||||
);
|
||||
expect(createdLink.sourceFingerprint, 'whatsapp|u:wa:taylor quinn');
|
||||
expect(
|
||||
afterFirst.sourceLinks.any(
|
||||
(SourceProfileLink link) => link.profileId == first.profileId,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
afterFirst.moments.any(
|
||||
afterCreate.moments.any(
|
||||
(RelationshipMoment moment) =>
|
||||
moment.personId == first.profileId && moment.type == 'whatsapp',
|
||||
moment.personId == created.profileId && moment.type == 'whatsapp',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
@@ -219,11 +231,11 @@ void main() {
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(second.createdProfile, isFalse);
|
||||
expect(second.profileId, first.profileId);
|
||||
expect(afterSecond.people.length, afterFirst.people.length);
|
||||
expect(second.profileId, created.profileId);
|
||||
expect(afterSecond.people.length, afterCreate.people.length);
|
||||
expect(
|
||||
afterSecond.sharedMessages.length,
|
||||
afterFirst.sharedMessages.length + 1,
|
||||
afterCreate.sharedMessages.length + 1,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -234,7 +246,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await _addTestPerson(container, name: 'Nora Diaz');
|
||||
|
||||
final SharedMessageIngestResult result = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -285,10 +297,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'She loves vegan sushi and rooftop dinners.',
|
||||
sourceApp: 'share_sheet',
|
||||
@@ -378,6 +387,35 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'captureSharedPayloadByCreatingProfile requires an explicit person name',
|
||||
() async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Alex: Kaffee klingt gut',
|
||||
sourceApp: 'whatsapp',
|
||||
platform: 'ios',
|
||||
);
|
||||
|
||||
expect(payload.sourceDisplayName, 'Alex');
|
||||
expect(
|
||||
() => container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.captureSharedPayloadByCreatingProfile(payload: payload),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
final LocalDataState after = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(after.people, isEmpty);
|
||||
expect(after.sharedMessages, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'queues near-match conflict and then uses fingerprint mapping for follow-up shares',
|
||||
() async {
|
||||
@@ -689,10 +727,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.ideas.length;
|
||||
|
||||
await container
|
||||
@@ -740,10 +778,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.reminders.length;
|
||||
|
||||
await container
|
||||
@@ -852,6 +890,28 @@ ProviderContainer _createContainer({ReminderScheduler? scheduler}) {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _addTestPerson(
|
||||
ProviderContainer container, {
|
||||
String name = 'Test Person',
|
||||
String relationship = 'Friend',
|
||||
}) async {
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: name,
|
||||
relationship: relationship,
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
return state.people
|
||||
.firstWhere((PersonProfile person) => person.name == name)
|
||||
.id;
|
||||
}
|
||||
|
||||
class RecordingReminderScheduler implements ReminderScheduler {
|
||||
final List<List<ReminderRule>> snapshots = <List<ReminderRule>>[];
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ void main() {
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final initial = await container.read(localRepositoryProvider.future);
|
||||
expect(initial.people, isNotEmpty);
|
||||
expect(initial.people, isEmpty);
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
|
||||
@@ -30,6 +30,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Share Inbox'), findsOneWidget);
|
||||
expect(find.text('Capture'), findsOneWidget);
|
||||
expect(find.text('Ambiguous match'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Review & Save'), findsOneWidget);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'triggers sync when reachability transitions from offline to online',
|
||||
'does not trigger sync when background sync is disabled by default',
|
||||
(WidgetTester tester) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: false);
|
||||
addTearDown(reachability.dispose);
|
||||
@@ -39,56 +39,56 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
// Startup trigger always runs once when background sync is enabled.
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(false);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 40));
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
expect(syncCalls, 2);
|
||||
expect(syncCalls, 0);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('does not trigger extra sync while connectivity stays online', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: true);
|
||||
addTearDown(reachability.dispose);
|
||||
testWidgets(
|
||||
'does not trigger sync while connectivity stays online by default',
|
||||
(WidgetTester tester) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: true);
|
||||
addTearDown(reachability.dispose);
|
||||
|
||||
int syncCalls = 0;
|
||||
int syncCalls = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
networkReachabilityProvider.overrideWithValue(reachability),
|
||||
syncCoordinatorProvider.overrideWith(
|
||||
(Ref ref) => _TestSyncCoordinator(
|
||||
ref,
|
||||
onSyncNow: () async {
|
||||
syncCalls += 1;
|
||||
return _okResult();
|
||||
},
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
networkReachabilityProvider.overrideWithValue(reachability),
|
||||
syncCoordinatorProvider.overrideWith(
|
||||
(Ref ref) => _TestSyncCoordinator(
|
||||
ref,
|
||||
onSyncNow: () async {
|
||||
syncCalls += 1;
|
||||
return _okResult();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SyncBackgroundRunner(child: SizedBox()),
|
||||
),
|
||||
],
|
||||
child: const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SyncBackgroundRunner(child: SizedBox()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
expect(syncCalls, 1);
|
||||
});
|
||||
expect(syncCalls, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _TestSyncCoordinator extends SyncCoordinator {
|
||||
|
||||
@@ -111,6 +111,29 @@ void main() {
|
||||
);
|
||||
expect(queueState.cursor, 'cursor-remote-1');
|
||||
});
|
||||
|
||||
test('default fake pull is inert and does not seed people', () async {
|
||||
final ProviderContainer container = ProviderContainer(
|
||||
overrides: [
|
||||
backendGatewayProvider.overrideWithValue(BackendGatewayFake()),
|
||||
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
|
||||
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
|
||||
final SyncRunResult result = await container
|
||||
.read(syncCoordinatorProvider)
|
||||
.pullRemote();
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.pulled, 0);
|
||||
|
||||
final localState = await container.read(localRepositoryProvider.future);
|
||||
expect(localState.people, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class _TestGateway extends BackendGatewayFake {
|
||||
|
||||
@@ -23,6 +23,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Repair Person',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final localBefore = await container.read(localRepositoryProvider.future);
|
||||
final person = localBefore.people.first;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user