@@ -147,9 +147,9 @@ NEW_FILES=(
147147
148148# Modified files to patch via git diff | git apply --3way
149149# Excludes: project.pbxproj (handled by Python script), SettingsView.swift (anchor-based),
150+ # LoopDataManager.swift (anchor-based — L&L Customizations modify this file heavily),
150151# and Localizable.xcstrings (direct checkout — too large for 3-way merge on JSON)
151152PATCH_FILES=(
152- " Loop/Managers/LoopDataManager.swift"
153153 " Loop/View Controllers/StatusTableViewController.swift"
154154 " Loop/View Models/AddEditFavoriteFoodViewModel.swift"
155155 " Loop/View Models/CarbEntryViewModel.swift"
@@ -222,8 +222,8 @@ validate_environment() {
222222 die " SettingsView.swift not found at expected path."
223223 fi
224224
225- if ! grep -q ' ForEach(pluginMenuItems\.filter {\$0\.section == \.configuration}) ' " $settings_file " ; then
226- die " Anchor not found in SettingsView.swift: ForEach(pluginMenuItems.filter { \$ 0.section == .configuration})
225+ if ! grep -q ' Diabetes Treatment ' " $settings_file " ; then
226+ die " Anchor not found in SettingsView.swift: Diabetes Treatment
227227 Your Loop version may be incompatible."
228228 fi
229229
@@ -426,8 +426,10 @@ with open(settings_path, "r") as f:
426426
427427lines = content.split("\n")
428428
429- # ─── Anchor 1: Insert feature rows BEFORE the ForEach(pluginMenuItems.filter configuration) ───
430- # This is inside configurationSection. We insert FoodFinder, LoopInsights, and AutoPresets rows.
429+ # ─── Anchor 1: Insert feature rows AFTER the Therapy Settings button ───
430+ # We anchor on "Diabetes Treatment" (the Therapy Settings descriptive text) so our
431+ # features appear right after Therapy Settings. If L&L Profiles is installed, it
432+ # inserts before the ForEach — so Profiles ends up BELOW our features.
431433
432434FEATURE_ROWS = """
433435 foodFinderSettingsRow
@@ -445,7 +447,7 @@ FEATURE_ROWS = """
445447 }
446448"""
447449
448- anchor1 = 'ForEach(pluginMenuItems.filter {$0.section == .configuration}) '
450+ anchor1 = 'Diabetes Treatment '
449451anchor1_idx = None
450452for i, line in enumerate(lines):
451453 if anchor1 in line:
@@ -456,11 +458,12 @@ if anchor1_idx is None:
456458 print(f"ERROR: Anchor 1 not found: {anchor1}", file=sys.stderr)
457459 sys.exit(1)
458460
459- # Insert the feature rows BEFORE the ForEach line
461+ # Insert the feature rows AFTER the Therapy Settings descriptive text line
460462feature_lines = FEATURE_ROWS.rstrip("\n").split("\n")
463+ insert_at = anchor1_idx + 2 # after the NavigationLink closing brace (line after "Diabetes Treatment")
461464for j, fl in enumerate(feature_lines):
462- lines.insert(anchor1_idx + j, fl)
463- print(f" Inserted {len(feature_lines)} lines before ForEach anchor (line {anchor1_idx + 1})")
465+ lines.insert(insert_at + j, fl)
466+ print(f" Inserted {len(feature_lines)} lines after Therapy Settings (line {anchor1_idx + 1})")
464467
465468# ─── Anchor 2: Insert computed properties BEFORE "private var cgmChoices:" ───
466469
@@ -479,18 +482,16 @@ COMPUTED_PROPS = """
479482 }
480483
481484 private var loopInsightsSection: some View {
482- Section {
483- NavigationLink(destination: LoopInsights_SettingsView(dataStoresProvider: viewModel.loopInsightsDataStores)) {
484- LargeButton(action: {},
485- includeArrow: false,
486- imageView: Image(systemName: "brain.head.profile")
487- .resizable()
488- .aspectRatio(contentMode: .fit)
489- .foregroundColor(Color(red: 26/255, green: 138/255, blue: 158/255))
490- .frame(width: 30),
491- label: NSLocalizedString("LoopInsights", comment: "LoopInsights settings button"),
492- descriptiveText: NSLocalizedString("AI-powered therapy settings analysis", comment: "LoopInsights settings descriptive text"))
493- }
485+ NavigationLink(destination: LoopInsights_SettingsView(dataStoresProvider: viewModel.loopInsightsDataStores)) {
486+ LargeButton(action: {},
487+ includeArrow: false,
488+ imageView: Image(systemName: "brain.head.profile")
489+ .resizable()
490+ .aspectRatio(contentMode: .fit)
491+ .foregroundColor(Color(red: 26/255, green: 138/255, blue: 158/255))
492+ .frame(width: 30),
493+ label: NSLocalizedString("LoopInsights", comment: "LoopInsights settings button"),
494+ descriptiveText: NSLocalizedString("AI-powered therapy settings analysis", comment: "LoopInsights settings descriptive text"))
494495 }
495496 }
496497
@@ -528,6 +529,126 @@ PYTHON_SCRIPT
528529 fi
529530}
530531
532+ # ─── Phase 6b: Patch LoopDataManager.swift (Anchor-Based) ────────────────────
533+ #
534+ # L&L Customizations heavily modify LoopDataManager.swift (Negative Insulin Damper,
535+ # function signature changes, etc.), so git apply --3way fails silently.
536+ # Instead, we use anchor-based insertion like SettingsView.swift.
537+
538+ patch_loop_data_manager () {
539+ header " Phase 6b: Patching LoopDataManager.swift (anchor-based)"
540+
541+ local ldm_file=" Loop/Loop/Managers/LoopDataManager.swift"
542+
543+ if [[ ! -f " $ldm_file " ]]; then
544+ die " LoopDataManager.swift not found at: $ldm_file "
545+ fi
546+
547+ # Skip if already patched
548+ if grep -q " AutoPresetsCoordinator" " $ldm_file " ; then
549+ info " LoopDataManager.swift already contains AutoPresets code — skipping."
550+ return 0
551+ fi
552+
553+ python3 - " $ldm_file " << 'PYTHON_SCRIPT '
554+ import sys
555+
556+ ldm_path = sys.argv[1]
557+
558+ with open(ldm_path, "r") as f:
559+ content = f.read()
560+
561+ lines = content.split("\n")
562+
563+ # ─── Anchor 1: Insert delegate setup after "self.trustedTimeOffset = trustedTimeOffset" ───
564+ # This is in the init method. The delegate line goes right after this assignment,
565+ # before the LiveActivity setup.
566+
567+ DELEGATE_SETUP = """\
568+
569+ // Set up AutoPresets coordinator delegate
570+ AutoPresetsCoordinator.shared.delegate = self
571+ """
572+
573+ anchor1 = "self.trustedTimeOffset = trustedTimeOffset"
574+ anchor1_idx = None
575+ for i, line in enumerate(lines):
576+ if anchor1 in line:
577+ anchor1_idx = i
578+ break
579+
580+ if anchor1_idx is None:
581+ print(f"ERROR: Anchor not found: {anchor1}", file=sys.stderr)
582+ sys.exit(1)
583+
584+ delegate_lines = DELEGATE_SETUP.rstrip("\n").split("\n")
585+ insert_at = anchor1_idx + 1
586+ for j, dl in enumerate(delegate_lines):
587+ lines.insert(insert_at + j, dl)
588+ print(f" Inserted delegate setup ({len(delegate_lines)} lines) after line {anchor1_idx + 1}")
589+
590+ # ─── Anchor 2: Append AutoPresetsDelegate extension at end of file ───
591+ # We find the very last closing brace of the file and append after it.
592+
593+ DELEGATE_EXTENSION = """
594+ // MARK: - AutoPresetsDelegate
595+
596+ extension LoopDataManager: AutoPresetsDelegate {
597+
598+ func autoPresets(_ coordinator: AutoPresetsCoordinator,
599+ shouldActivatePreset preset: TemporaryScheduleOverridePreset) {
600+ logger.default("AutoPresets activating preset: %{public}@", preset.name)
601+
602+ mutateSettings { settings in
603+ settings.scheduleOverride = preset.createOverride(enactTrigger: .local)
604+ }
605+ }
606+
607+ func autoPresets(_ coordinator: AutoPresetsCoordinator,
608+ shouldDeactivatePreset preset: TemporaryScheduleOverridePreset) {
609+ guard let currentOverride = settings.scheduleOverride,
610+ case let .preset(currentPreset) = currentOverride.context,
611+ currentPreset.id == preset.id
612+ else {
613+ return
614+ }
615+
616+ logger.default("AutoPresets deactivating preset: %{public}@", preset.name)
617+
618+ mutateSettings { settings in
619+ settings.scheduleOverride = nil
620+ }
621+ }
622+
623+ func autoPresetsAvailablePresets(_ coordinator: AutoPresetsCoordinator) -> [TemporaryScheduleOverridePreset] {
624+ settings.overridePresets
625+ }
626+
627+ func autoPresetsCurrentOverride(_ coordinator: AutoPresetsCoordinator) -> TemporaryScheduleOverride? {
628+ settings.scheduleOverride
629+ }
630+ }
631+ """
632+
633+ extension_lines = DELEGATE_EXTENSION.split("\n")
634+ lines.extend(extension_lines)
635+ print(f" Appended AutoPresetsDelegate extension ({len(extension_lines)} lines) at end of file")
636+
637+ # Write back
638+ with open(ldm_path, "w") as f:
639+ f.write("\n".join(lines))
640+
641+ print(" LoopDataManager.swift patched successfully.")
642+ PYTHON_SCRIPT
643+
644+ if [[ $? -eq 0 ]]; then
645+ success " LoopDataManager.swift patched with AutoPresets delegate"
646+ else
647+ error " Failed to patch LoopDataManager.swift"
648+ return 1
649+ fi
650+ }
651+
531652# ─── Phase 7: Update project.pbxproj ─────────────────────────────────────────
532653
533654update_pbxproj () {
@@ -669,7 +790,7 @@ cleanup() {
669790 echo -e " ${BOLD} Next steps:${NC} "
670791 echo " 1. Open LoopWorkspace.xcworkspace in Xcode"
671792 echo " 2. Build and run (Cmd+R)"
672- echo " 3. In the Loop app: Settings → enable FoodFinder / LoopInsights"
793+ echo " 3. In Loop > Settings > Enable AutoPresets / FoodFinder / LoopInsights"
673794 echo " 4. Enter your AI API key in FoodFinder Settings"
674795 echo " "
675796 echo -e " ${BOLD} To uninstall:${NC} "
@@ -774,6 +895,7 @@ main() {
774895 install_new_files
775896 patch_modified_files
776897 patch_settings_view
898+ patch_loop_data_manager
777899 update_pbxproj
778900 validate_installation
779901 cleanup
0 commit comments