Skip to content

fix: repair broken UX flows and dead ends across the app - #25

Open
FlazeIGuess wants to merge 3 commits into
mainfrom
claude/project-analysis-ux-fixes-it0wmg
Open

fix: repair broken UX flows and dead ends across the app#25
FlazeIGuess wants to merge 3 commits into
mainfrom
claude/project-analysis-ux-fixes-it0wmg

Conversation

@FlazeIGuess

Copy link
Copy Markdown
Owner

Fixes flow bugs and gaps found while auditing the app end to end. No release is created by this PR — the APK is uploaded as a workflow artifact under the Actions run only.

Sharing / processing

  • Endless spinner fixed. Five code paths set _error without clearing _isLoading, so build kept rendering the loading state. Hitting "song not available on your service" left the user stuck forever with no way out.
  • Error state gained a Try Again action; loading state gained a Cancel. The result column is now scrollable, so the alternative-service list cannot overflow on short screens.
  • The pulsing logo used TweenAnimationBuilder with a fixed tween and an onEnd that called setState; the tween never changed so it pulsed once and froze. Replaced with a repeating controller.
  • The metadata fast path launched the sender's platform link verbatim, ignoring the user's preferred service — the exact problem the app exists to solve. It now converts unless the link already points at the preferred service.

Playlists

  • Received playlists were single-platform. The backend stores only the creator's original URL, so every imported track opened the creator's service. Links are now resolved on first open and cached back into storage.
  • Import screen had no try/catch around a bare HTTP call and an error state with no actions — a failed import was an infinite spinner or a dead end. Now: timeouts, retry, and a way back.
  • Re-opening the same share link created a duplicate copy each time; imports are now deduplicated by remote id.
  • A failed publish was silent, dropping the user on a share screen whose share button could only fail. It now says so.
  • Closing the creator discarded an in-progress playlist without asking, including via system back.
  • Title/description limits were shown as errors but did not block creation.
  • Tracks added from history were stored with empty convertedLinks, making them unplayable for anyone on another service.
  • context.go in the create/detail/post-create flow wiped the navigation stack, so Back exited the app. Replaced with pushReplacement/pop.
  • 24×24 icon-only share/QR buttons replaced with labelled 48dp targets; added pull to refresh.

History

  • No way to delete a single entry. onEntryDelete was wired up but never reachable — only "Clear All" existed. Added a delete action with undo.
  • Re-convert actions showed a 1–2s SnackBar for a call that can take ~30s, so the screen looked idle and repeated taps fired parallel requests. Now behind a blocking progress dialog with a re-entrancy guard.
  • Re-sharing dropped the nickname that a fresh share includes, and recorded nothing, so share statistics under-reported.
  • An active service filter with no matches showed "No shares yet", which reads like data loss. Now filter-aware with a "Clear filter" action.

Home / navigation / settings

  • Back from any tab exited the app instead of returning Home.
  • The tutorial button used context.go, replacing the stack with an onboarding flow that had no exit — users were trapped until they walked every remaining step. It is now a pushed flow with a close button.
  • A Timer.periodic rebuilt the whole Home screen every 3s forever, including once the rotating placeholder was no longer displayed.
  • Link field: no keyboard submit, no clear button, and the consumed link stayed behind after sharing.
  • The nickname TextEditingController was constructed inside build — leaked on every rebuild and reset the caret mid-typing. Now owned by a StatefulWidget, with debounced writes.
  • The version string was hardcoded; it now comes from package metadata.
  • "Configure Link Handling" jumped into system settings with no explanation; the steps are shown first.
  • The support dialog claimed the app is "ad-free". It is ad-supported.

Platform / plumbing

  • <queries> declared no custom-scheme intents, so canLaunchUrl reported installed apps as missing on Android 11+. SMS had no <package> entry at all, so that messenger choice always silently fell back to the system share sheet.
  • link_encoder.dart used print in 19 places, leaking user URLs and nicknames into release logcat.
  • Playlist backend calls had no timeouts and unchecked casts.
  • A return instead of continue abandoned a whole multi-item share if the first item was a deep link.
  • A bare '/p/' substring test matched non-UniTune URLs; now host-scoped.

Verification

  • flutter analyze — no errors, no warnings (21 remaining infos are pre-existing, mostly avoid_print in unused *_example.dart files).
  • flutter test — 6/6 pass, including two new cases covering import deduplication and persisted link conversion.

Not addressed (flagged for follow-up)

  • ~1,500 lines of dead code ship in the app: processing_screen_v2.dart, share_intent_handler.dart, track_search_screen.dart, batch_conversion_service.dart, cover_collage_generator.dart and the *_example.dart files are referenced from nowhere.
  • Deleting a playlist keeps the remote copy alive, so its share link keeps working; a deleteToken is stored but never used.
  • README advertises "3 to 10 tracks" per playlist; the code enforces no bounds at all.
  • AdHelper.defaultRequest always sets nonPersonalizedAds: true, so the UMP consent flow's outcome is never applied.

Generated by Claude Code

Sharing / processing
- Fix the endless spinner: setting an error no longer leaves _isLoading true,
  so failures actually render an error state instead of hanging forever.
- Add "Try Again" and a Cancel action; make the result area scrollable so the
  alternative-service list cannot overflow small screens.
- Loop the loading pulse with a repeating controller instead of a
  TweenAnimationBuilder that fired once and froze.
- Honour the preferred music service on the metadata fast path; it previously
  opened the sender's platform link verbatim.

Playlists
- Resolve per-platform links lazily for received playlists so tracks open in
  the recipient's own music app, which is the point of the feature.
- Import screen: wrap in try/catch, add timeouts, and give the error state a
  retry plus a way back; re-opening a link now reuses the existing import
  instead of creating a duplicate.
- Warn when a playlist could not be published instead of silently dropping the
  user on a share screen whose share will fail.
- Confirm before discarding an in-progress playlist (close button and system
  back).
- Enforce the title/description limits that were already shown as errors.
- Fetch converted links for tracks added from history.
- Use pushReplacement/pop instead of go so Back no longer exits the app.
- Replace 24dp icon-only buttons with labelled 48dp targets; add pull to
  refresh.

History
- Add per-entry delete with undo; previously only "Clear All" existed.
- Block re-convert actions behind a progress dialog so repeated taps cannot
  fire parallel requests.
- Include the nickname when re-sharing and record the re-share so statistics
  stop under-reporting.
- Show a filter-aware empty state with a "Clear filter" action.

Home / navigation / settings
- Back now returns to the Home tab before leaving the app.
- Open the tutorial as a pushed flow with a close button so it is no longer a
  one-way trip.
- Stop the 3s rebuild timer once real data is available.
- Link field: keyboard submit, clear button, reset after sharing.
- Own the nickname controller in a StatefulWidget (was rebuilt and leaked on
  every build) and debounce writes.
- Read the version from package metadata instead of a hardcoded string.
- Explain the "Open by default" steps before jumping to system settings.
- Correct the support copy that claimed the app is ad-free.

Platform / plumbing
- Declare custom-scheme queries in the manifest; without them canLaunchUrl
  reported installed apps as missing (SMS in particular).
- Route link-encoder logging through debugPrint so URLs and nicknames stay out
  of release logs.
- Add timeouts and null-safe parsing to the playlist backend calls.
- Add an on-demand debug-APK workflow.
The debug build type now carries applicationIdSuffix ".dev", so a test APK
installs next to an existing release install instead of trying to replace it.

Both release and CI builds are debug-signed with keystores generated per
machine, so their signing certificates differ. An in-place update is therefore
rejected with INSTALL_FAILED_UPDATE_INCOMPATIBLE and forces an uninstall, which
wipes history, playlists and settings — all app state lives in SharedPreferences
and there is no export path.

- applicationIdSuffix ".dev" and versionNameSuffix "-dev" for debug builds.
- App label moved to a manifest placeholder: "UniTune" vs "UniTune Dev", so the
  two installs are distinguishable in the launcher, the share sheet and the
  link chooser, where both register the same intent filters.
- AdMob app id moved to a manifest placeholder as well. The ".dev" application
  id is not registered with AdMob, so debug builds point at Google's public
  test app id; USE_TEST_ADS switches the ad units to match. Production builds
  are unaffected: both placeholders default to the existing values.
- The dev workflow prints the APK's signer certificate, so its signing identity
  can be compared against an installed build without downloading it first.
The bar had no sliding indicator at all, despite the doc comment claiming one.
Each tab painted its own background when selected, so the highlight appeared to
jump because one background faded in while another faded out. Item widths were
also non-uniform and changed mid-transition, since only the selected tab
revealed its label via AnimatedSize. There was no stable geometry a drag could
follow.

Rebuilt around a single pill positioned by a continuous position value:

- Drag anywhere on the bar to move the pill; release snaps to the nearest item,
  and a flick carries to the neighbour.
- Tapping is unchanged and still wins the gesture arena when the finger does
  not move.
- Haptic detent each time a new item becomes the nearest, plus the existing
  impact on commit.
- Every item expands to the same width, derived from the widest measured label,
  and the expansion budget is split between the two items the pill straddles.
  The total width is therefore constant and nothing reflows during the drag.
- Labels reveal horizontally via a width factor rather than reflowing, so text
  never wraps or jumps.
- The bar shrinks the expansion budget to fit narrow screens instead of
  overflowing.
- External index changes (system back returning to Home, a screen jumping to
  the Playlists tab) animate the pill, guarded so the callback-triggered
  rebuild cannot restart the settle animation mid-flight.

Selection is committed on release, not during the drag, so the content
transition in MainShell is not thrashed while the finger moves.

Adds 7 widget tests covering tap, drag-to-next, snap-back below the threshold,
multi-slot drags, clamping at both ends, external index changes, and the
constant-width invariant.
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