Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .impeccable/surfaces/tutorial-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
version: 1
slug: "tutorial-guidance"
primary_target: "lib/features/onboarding/widgets/tutorial_overlay.dart"
related_targets: ["lib/providers/tutorial_provider.dart","lib/features/onboarding/tutorial_targets.dart","lib/features/onboarding/tutorial_navigation.dart"]
---

# Interactive tutorial

Visitor mode: Operate. Help listeners find and play their own music with real
controls, without requiring playback or setup to finish the guide.

## Direction contract

Keep the five welcome posters unchanged. This is compact guidance over the
existing Material dark library, settings, and player, not another poster screen.
Use the app's bundled font stack, paper text, one acid accent, and native
48dp-minimum buttons. Bound and scroll the tooltip for short windows, large text,
safe insets, and the keyboard. No decorative animation or perpetual motion.

The invitation is optional. Adapt the core route to an empty or populated
library, the configured search destination, and existing playback. Listeners
can use the highlighted control or Continue; Back and End tour remain available.
Finish with Done, not a compulsory manual launch. Independently replayable
library, player/queue, appearance, and audio guides live in the guide picker,
also accessible from the existing Interactive Tutorial setting.

Only relevant real actions advance a step. Observe successful playback rather
than requesting playback. Never change volume, queue, output, DSP, or user
preferences for a demonstration. Missing controls, loading, permissions, picker
cancellation, and playback failure must not trap the listener. Yield to modal
routes and Android permission/folder pickers; restore guidance on return.

Anchor actual rendered controls across portrait, rail, landscape, and player
layouts. Measure relative to the overlay after layout, not fixed delays. Keep
real controls hit-testable and retain native semantics. Deep-link existing
manual sections instead of duplicating their detailed instructions.

Persist dismissed and completed states separately per stable guide ID. Preserve
legacy completion and never downgrade completion when a replay is dismissed.
English and Chinese are localized; Taglish guidance and controls remain English.

## Verification boundary

Provider and widget tests cover guide adaptation/progression, unavailable
controls, real taps, persistence/migration, route/modal handoff, large text,
keyboard bounds, Chinese, and English guidance under Taglish. The existing
onboarding and landscape mini-player handoff regressions remain in scope.

Native Android visual, TalkBack, permission-picker, and playback sign-off is
still open: the emulator did not come online. No APK was installed on the
user's A063, and no device music data was touched. Do not describe widget
coverage or a successful APK build as native-device validation.
2 changes: 2 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Flick's poster visual system combines Replay color fields, large branded type, b

Scope is the onboarding/Replay-derived poster visual system only. Standard player, library, and settings screens retain their existing Material dark theme in `lib/core/theme/app_theme.dart`. Onboarding page order and exact compositions belong to `.impeccable/surfaces/onboarding-screens-onboarding-screen-dart-eb866e34.md`, not universal design rules.

Interactive tutorial guidance is separately scoped in `.impeccable/surfaces/tutorial-guidance.md`: compact Material tooltips with a restrained acid accent, not full-screen posters. It preserves the five welcome pages and the surrounding app theme.

The frozen implementation includes the reviewed font-wrapping fix. The handoff reports 25 passing onboarding tests using bundled fonts, including English/Taglish whole-word regressions at 320 × 568 with 2x text, and a clean targeted analyzer. The full suite has an unrelated Isar binary mismatch. Review PNGs are supplementary Flutter widget renders, not device screenshots or shipping assets. Native Android sign-off remains blocked: the emulator did not boot, the user's phone was untouched, and the final native gate is not closed.

**Key Characteristics:**
Expand Down
25 changes: 14 additions & 11 deletions docs/BAYBAYIN_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
**Draft, not linguistically verified.** The Filipino reviewer decides whether
the wording is natural and the Baybayin spelling is correct. Passing tests does
not approve a translation. The 2,507-message Latin resource, eleven curated
Baybayin labels, fifteen native-speaker-supplied Taglish hooks and six streak
greetings remain drafts for review. The latest corrections are screen-specific,
not a global replacement of Filipino with English.
Baybayin labels, fifteen native-speaker-supplied Taglish hooks, five fixed
streak greetings and one day-count streak greeting remain drafts for review. The
latest corrections are screen-specific, not a global replacement of Filipino
with English.

## Try it

Expand Down Expand Up @@ -105,7 +106,8 @@ subsequently turning them off survives a restart.
CSV/TXT recaps are always English regardless of UI language.
- The streak popup keeps its count label and both buttons in English
(`N day streak`, `Continue`, `Don't show again today`). Its message is one of
six Taglish greetings chosen at random each time the popup opens.
five fixed Taglish greetings or a day-count line, chosen at random each time
the popup opens.
- The Milestones screen, category names, progress units, dates, locked sheet and
celebration card are English in Taglish. The streak banner and its
**Current Streak** label are English too; the streak popup's greeting keeps
Expand Down Expand Up @@ -223,9 +225,10 @@ stability, locale isolation, review coverage and layout.
## Filipino-only streak greetings

The launch-day streak popup keeps its visible count label and both buttons in
English under Taglish. Its message is one of the six native-speaker-supplied
Taglish lines below, chosen at random each time the popup opens. English and
Chinese keep their existing tier-specific messages.
English under Taglish. Its message is one of the five fixed
native-speaker-supplied Taglish lines below or a line that fills in the current
streak, chosen at random each time the popup opens. English and Chinese keep
their existing tier-specific messages.

| Original Taglish greeting | Status |
|---|---|
Expand All @@ -234,7 +237,7 @@ Chinese keep their existing tier-specific messages.
| Handa mo na earphones mo ulit. Hahaha. | draft |
| Hobby mo talaga music, maem? | draft |
| Share your music with? | draft |
| Baka mamaya puro ka relapse ha? | draft |
| {days} days na ha? Keep it up! | draft |

Edit `lib/l10n/streak_greeting.dart` and the corresponding rows together.

Expand Down Expand Up @@ -342,8 +345,8 @@ host widget renders with bundled fonts, not a substitute for Android hardware.

Before removing the experimental/draft notices, the Filipino reviewer should:

- [ ] Review Latin Filipino wording, all eleven Baybayin spellings, home hooks
and the six streak greetings.
- [ ] Review Latin Filipino wording, all eleven Baybayin spellings, home hooks,
the five fixed streak greetings and the day-count streak line.
- [ ] Test Android API 26 and a current Android version, including bold text,
combining/final marks, narrow widths and large system font sizes.
- [ ] Switch Taglish ↔ English ↔ Chinese and enable/disable headings live.
Expand Down Expand Up @@ -375,7 +378,7 @@ Before removing the experimental/draft notices, the Filipino reviewer should:
verify actual Filipino pronunciation rather than assuming language support.
- [ ] Check home hooks across time boundaries and day changes, including long
hooks at enlarged text settings. Confirm the streak popup rotates through
all six lines and fits the longest greeting.
the five fixed lines and the day-count line and fits the longest greeting.
- [ ] Check native dialogs, notifications and home-screen widgets separately for
Latin Filipino font support, clipping and stale strings after switching.

Expand Down
20 changes: 15 additions & 5 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:flick/providers/equalizer_provider.dart';
import 'package:flick/providers/providers.dart';
import 'package:flick/features/onboarding/screens/onboarding_screen.dart';
import 'package:flick/features/onboarding/tutorial_targets.dart';
import 'package:flick/features/onboarding/tutorial_navigation.dart';
import 'package:flick/features/onboarding/widgets/tutorial_overlay.dart';
import 'package:flick/widgets/common/embedded_mini_player.dart';
import 'package:flick/widgets/common/landscape_mini_player_dock.dart';
Expand Down Expand Up @@ -75,6 +76,7 @@ class FlickPlayerApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: AppTheme.darkTheme,
navigatorKey: rootNavigatorKey,
navigatorObservers: [tutorialNavigation.root],
localizationsDelegates: kAppLocalizationsDelegates,
supportedLocales: kSupportedLocales,
localeResolutionCallback: resolveAppLocale,
Expand All @@ -101,7 +103,9 @@ class FlickPlayerApp extends StatelessWidget {
(preferences) => preferences.connectionNoticesEnabled,
),
),
child: child ?? const SizedBox.shrink(),
child: TutorialHost(
child: child ?? const SizedBox.shrink(),
),
),
),
);
Expand Down Expand Up @@ -461,6 +465,7 @@ class _MainShellState extends ConsumerState<MainShell>
appPreferencesProvider,
(previous, next) {
if (!mounted) return;
if (ref.read(tutorialProvider).active) return;
final notifier = ref.read(whatsNewProvider.notifier);
notifier.evaluate();
final pending = ref.read(whatsNewProvider).pendingEntry;
Expand Down Expand Up @@ -749,6 +754,11 @@ class _MainShellState extends ConsumerState<MainShell>
void _handleBackPress(bool didPop, dynamic result) {
if (didPop) return;

if (ref.read(tutorialProvider).active && tutorialNavigation.visible) {
ref.read(tutorialProvider.notifier).skip();
return;
}

// Back events only reach the root navigator; detail screens pushed on
// the nested tab navigator must be popped here manually.
final nested = nestedNavigatorKey.currentState;
Expand Down Expand Up @@ -898,7 +908,10 @@ class _MainShellState extends ConsumerState<MainShell>
child: Navigator(
key: nestedNavigatorKey,
initialRoute: '/',
observers: [_navBarVisibilityObserver],
observers: [
_navBarVisibilityObserver,
tutorialNavigation.nested,
],
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder<void>(
Expand Down Expand Up @@ -1118,9 +1131,6 @@ class _MainShellState extends ConsumerState<MainShell>
// Pinned scan/preload progress bubble (overlay minimized)
const Positioned.fill(child: FloatingScanProgress()),

// Interactive tutorial overlay
const Positioned.fill(child: TutorialOverlay()),

// USB DAC attach → auto bit-perfect switch (decline-aware)
const UsbBitPerfectPrompt(),
],
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/navigation_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class NavigationHelper {
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const QueueScreen(),
settings: const RouteSettings(name: '/queue'),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
if (AppConstants.animationNormal == Duration.zero) {
return child;
Expand Down
16 changes: 14 additions & 2 deletions lib/features/manual/screens/manual_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ class _ManualScreenState extends ConsumerState<ManualScreen> {
@override
Widget build(BuildContext context) {
final currentSong = ref.watch(currentSongProvider);
// Put a deep-linked section first so it is built even in a lazy list.
// The normal manual keeps its established reading order.
final sections = widget.initialSection == null
? kManualSections
: [
...kManualSections.where(
(section) => section.id == widget.initialSection,
),
...kManualSections.where(
(section) => section.id != widget.initialSection,
),
];
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
Expand All @@ -100,9 +112,9 @@ class _ManualScreenState extends ConsumerState<ManualScreen> {
horizontal: AppConstants.spacingMd,
vertical: AppConstants.spacingMd,
),
itemCount: kManualSections.length,
itemCount: sections.length,
itemBuilder: (context, i) {
final section = kManualSections[i];
final section = sections[i];
final initiallyExpanded =
widget.initialSection == section.id;
return _ManualSectionCard(
Expand Down
7 changes: 4 additions & 3 deletions lib/features/milestone/widgets/streak_popup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ class _StreakPopupState extends State<StreakPopup>
late final AnimationController _pulse;
late final AnimationController _flame;

/// One supplied Taglish greeting per opening. Kept for English and Chinese
/// too, but only shown when the copy role is Filipino.
late final String _taglishGreeting = randomTaglishStreakGreeting();
/// One greeting per opening. Kept for English and Chinese too, but only
/// shown when the copy role is Filipino.
late final String _taglishGreeting;

@override
void initState() {
super.initState();
_taglishGreeting = randomTaglishStreakGreeting(widget.streak);
_reveal = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1100),
Expand Down
81 changes: 81 additions & 0 deletions lib/features/onboarding/tutorial_navigation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';

/// Both stacks report here, so a nested sheet also suspends guidance.
class TutorialNavigation extends ChangeNotifier {
final root = TutorialRouteObserver();
final nested = TutorialRouteObserver();
TutorialNavigation() {
root.onChanged = notifyListeners;
nested.onChanged = notifyListeners;
}
String? get currentName => root.top?.isFirst == true
? nested.top?.settings.name ?? root.top?.settings.name
: root.top?.settings.name;
bool get visible {
final route = root.top;
if (route == null || route is PopupRoute || nested.top is PopupRoute) {
return false;
}
final name = currentName;
return (route.isFirst && (nested.top?.isFirst ?? true)) ||
name == '/full_player' ||
name == '/queue' ||
(name?.startsWith('/tutorial/') ?? false);
}

@override
void dispose() {
root.onChanged = null;
nested.onChanged = null;
super.dispose();
}
}

class TutorialRouteObserver extends NavigatorObserver {
final List<Route<dynamic>> _routes = [];
VoidCallback? onChanged;
Route<dynamic>? get top => _routes.lastOrNull;
bool _scheduled = false;
void _changed() {
if (_scheduled) return;
_scheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_scheduled = false;
onChanged?.call();
});
}

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (previousRoute == null) _routes.clear();
_routes.add(route);
_changed();
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
_routes.remove(route);
_changed();
}

@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
_routes.remove(route);
_changed();
}

@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
final index = oldRoute == null ? -1 : _routes.indexOf(oldRoute);
if (index >= 0) {
if (newRoute == null) {
_routes.removeAt(index);
} else {
_routes[index] = newRoute;
}
}
_changed();
}
}

final tutorialNavigation = TutorialNavigation();
Loading
Loading