Skip to content

feat: add Add item and Try it OS shortcuts - #31

Merged
jvsena42 merged 4 commits into
mainfrom
feat/os-shortcuts
Jul 27, 2026
Merged

feat: add Add item and Try it OS shortcuts#31
jvsena42 merged 4 commits into
mainfrom
feat/os-shortcuts

Conversation

@jvsena42

Copy link
Copy Markdown
Owner

Summary

Adds OS-level shortcuts for the app's two highest-intent actions: Add item and Try it, reachable by long-pressing the app icon on both platforms.

Worn already proves it can be entered from outside the app — the system-share path drops a photo straight into Try It. This adds the other outside entry point, the icon itself. Both shortcuts land the user on the right surface and stop there: no camera or picker is auto-fired. Add item opens the Wardrobe tab with the Add Item sheet up; Try it opens the Try It tab idle.

There is no navigation library here — navigation is a flat tab switch plus screen-local sheet booleans — so a shortcut is expressed as select a tab plus, optionally, set a screen's sheet flag. The work reuses the "pending command hoisted at the entry point, threaded down, consumed exactly once" pattern that SharedPhoto established.

Changes

  • Shared — new AppShortcut enum in commonMain. Android's intent action and iOS's UIApplicationShortcutItem.type are bare strings crossing a platform boundary, so the ids are declared once and each platform maps its own identifier onto them. Covered by AppShortcutTest.
  • Android — static shortcuts in res/xml/shortcuts.xml (new directory) plus the android.app.shortcuts meta-data on MainActivity. Static, so they exist from install and localize through @string with no runtime code. The intents are explicit (targetPackage + targetClass), so the custom actions are a payload for MainActivity to read rather than something the system has to resolve — no new intent-filter.
  • AndroidhandleShareIntent generalised to handleIntent; a new ShortcutCommand (deliberately not a data class, so identity equality re-fires LaunchedEffect on a repeat tap, same as SharedPhoto) is hoisted in MainActivity and threaded through App into WardrobeScreen.
  • Android — two new Lucide-style vector drawables. They bake in the ink colour rather than the existing icons' placeholder black, because a launcher does not tint a shortcut icon.
  • iOS — new Services/QuickActions.swift: the actions are registered at launch rather than declared in Info.plist, so titles come from Localizable.strings like every other user-facing string and the icons are SF Symbols like the rest of the app. Trade-off: they appear after the first launch, not at install.
  • iOS — a minimal AppDelegate + SceneDelegate pair, since SwiftUI has no hook for performActionFor. Both delivery paths are covered: willConnectTo on a cold launch, windowScene(_:performActionFor:) while running.
  • Localization — four new keys (shortcut_add_item_short/long, shortcut_try_it_short/long) in all four files. The pt-BR short label is "Adicionar" rather than "Adicionar item", because launchers truncate short labels hard.

Two notes on consuming the command exactly once, both load-bearing:

  • Android's pager sets beyondViewportPageCount = 1, so it disposes Wardrobe when it is more than a page away. An unconsumed command would reopen the sheet on every return to the tab — hence onAddSheetOpened.
  • On iOS a tap is marked handled by id instead of cleared on the inbox: the @Published replay can land mid-update, and SwiftUI forbids publishing changes from there. WardrobeScreen uses .task(id:) rather than .onChange so a command already set when the view first appears is not missed.

Test plan

Automated (run and passing):

  • ./gradlew :shared:allTestsAppShortcutTest, 5 tests
  • ./gradlew detekt
  • ./gradlew :composeApp:assembleDebug

Android — ./gradlew :composeApp:installDebug, then:

  • Long-press the launcher icon: both shortcuts appear with the right icons and labels.
  • Add item from cold start → Wardrobe tab, Add Item sheet open. Try it from cold start → Try It tab, idle.
  • With the app already running on the Settings tab, tap each shortcut → correct tab, and only one activity instance (adb shell dumpsys activity activities | grep MainActivity).
  • Replay regression: after Add item, dismiss the sheet, swipe to Settings and back to Wardrobe — the sheet must not reopen.
  • Switch the device to pt-BR and re-check both labels.
  • Confirm the existing share path still works (share a photo from Photos → Try It).

Shortcut intents can also be driven directly:

adb shell am start -a com.github.worn.action.add_item -n com.github.worn/.MainActivity

iOS — open iosApp/ in Xcode, run once (dynamic actions need one launch), then long-press the Home Screen icon:

  • Both actions appear with the plus / viewfinder symbols and localized titles.
  • Test both paths: warm (app backgrounded) → performActionFor; cold (swipe the app away first) → willConnectTo. They are separate delegate callbacks.
  • Same dismiss-and-return regression check on the Wardrobe sheet.
  • Re-run with the scheme's App Language set to Portuguese (Brazil).

Checklist

  • ./gradlew detekt passes
  • Tested on Android
  • Tested on iOS
  • Updated documentation (if applicable)

Not yet verified: the iOS code is unbuilt — it was written on a machine without Xcode, so QuickActions.swift and the two Swift edits still need a compile and a device run before this merges. The Android side builds and the shared tests pass.

🤖 Generated with Claude Code

jvsena42 and others added 4 commits July 27, 2026 18:13
Android's intent action and iOS's UIApplicationShortcutItem type are bare
strings crossing a platform boundary, so they are declared once in commonMain
and each platform maps its own identifier onto them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Static shortcuts, so they exist from install and localize through @string with
no runtime code. The intents are explicit, so the custom actions are a payload
for MainActivity to read rather than something the system has to resolve.

ShortcutCommand is deliberately not a data class, for the same reason as
SharedPhoto: identity equality re-fires LaunchedEffect when the same shortcut
is tapped twice in a row.

Try It needs only the tab switch, so App consumes it. Add Item is threaded down
to WardrobeScreen, which consumes it once the sheet is open — the pager sets
beyondViewportPageCount = 1, so an unconsumed command would reopen the sheet on
every return to the tab.

The shortcut drawables bake in the ink colour instead of the existing icons'
placeholder black: a launcher does not tint a shortcut icon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registered at launch rather than declared in Info.plist so the titles come from
Localizable.strings like every other user-facing string and the icons are SF
Symbols like the rest of the app. They appear after the first launch, not at
install, which is the accepted cost.

SwiftUI has no hook for performActionFor, so a minimal app/scene delegate pair
is attached. Both delivery paths are covered: willConnectTo on a cold launch and
windowScene(_:performActionFor:) while running.

A tap is marked handled by id instead of cleared on the inbox, because the
@published replay can land mid-update and SwiftUI forbids publishing changes
from there. WardrobeScreen uses task(id:) rather than onChange so a command
already set when the view first appears is not missed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A plain drawable is scaled onto a white badge by the launcher, which is why the
glyphs had to carry dark ink. Going adaptive means the shortcut owns its
background, so a white glyph stays legible whatever the launcher's theme, and a
pinned shortcut matches the app icon instead of sitting on a white disc.

The background reuses the launcher icon's green for exactly that reason. The
monochrome layer opts into themed icons on Android 13+.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 merged commit 1649a3a into main Jul 27, 2026
1 check passed
@jvsena42
jvsena42 deleted the feat/os-shortcuts branch July 27, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant