diff --git a/apps/desktop/lib/src/shell/app_shell.dart b/apps/desktop/lib/src/shell/app_shell.dart index 989703e..0482dc0 100644 --- a/apps/desktop/lib/src/shell/app_shell.dart +++ b/apps/desktop/lib/src/shell/app_shell.dart @@ -641,6 +641,13 @@ class AppShellState extends State with SingleTickerProviderStateMixin ); } + /// No Jellyfin account, no server joined or hosted yet, band not + /// dismissed: the welcome band explains the three ways in. + bool get _newcomer => + !AppSettings.instance.welcomeDismissed.value && + _jellyfin.isEmpty && + SessionState.instance.recents.value.isEmpty; + Widget _body(bool online, bool alone) { final coord = SessionState.instance.coordinator; final tabBody = switch (_tab) { @@ -651,12 +658,14 @@ class AppShellState extends State with SingleTickerProviderStateMixin solo: alone, // Offline, a put-up starts a solo session, which hosts. isHost: !online || coord.hostFile != null, - banner: online + // Only for newcomers: the strip has Join and Host for everyone else. + banner: online || !_newcomer ? null : WelcomeBand( onJoin: () => _connect(ConnectMode.join), onHost: () => _connect(ConnectMode.host), onSolo: _watchSolo, + onDismiss: () => setState(AppSettings.instance.dismissWelcome), ), jellyfinStart: _libraryStart, onPutUp: _putUp, diff --git a/apps/desktop/lib/src/shell/welcome.dart b/apps/desktop/lib/src/shell/welcome.dart index ffef960..e5d9c35 100644 --- a/apps/desktop/lib/src/shell/welcome.dart +++ b/apps/desktop/lib/src/shell/welcome.dart @@ -19,7 +19,8 @@ class WelcomeBand extends StatelessWidget { final VoidCallback onJoin; final VoidCallback onHost; final VoidCallback onSolo; - const WelcomeBand({super.key, required this.onJoin, required this.onHost, required this.onSolo}); + final VoidCallback? onDismiss; + const WelcomeBand({super.key, required this.onJoin, required this.onHost, required this.onSolo, this.onDismiss}); @override Widget build(BuildContext context) { @@ -40,6 +41,19 @@ class WelcomeBand extends StatelessWidget { Text(appName, style: t.titleLarge?.copyWith(fontWeight: FontWeight.w700, height: 1.1)), const SizedBox(height: 2), Text(appTagline, style: TextStyle(color: scheme.onSurfaceVariant)), + if (onDismiss != null) + TextButton( + key: const ValueKey('welcome-dismiss'), + onPressed: onDismiss, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(0, 24), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + foregroundColor: scheme.onSurfaceVariant, + textStyle: const TextStyle(fontSize: 12), + ), + child: const Text("Don't show again"), + ), ], ), ], @@ -76,7 +90,7 @@ class WelcomeBand extends StatelessWidget { ], ), ); - return Container( + final band = Container( key: const ValueKey('welcome-band'), margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), padding: const EdgeInsets.all(14), @@ -110,6 +124,7 @@ class WelcomeBand extends StatelessWidget { ); }), ); + return band; } } diff --git a/apps/desktop/lib/src/state/settings.dart b/apps/desktop/lib/src/state/settings.dart index 726d026..d9f2b7e 100644 --- a/apps/desktop/lib/src/state/settings.dart +++ b/apps/desktop/lib/src/state/settings.dart @@ -23,6 +23,7 @@ const _keyProgressLine = 'progressLine'; const _keySoundVolume = 'soundVolume'; const _keySoundsOff = 'soundsOff'; const _keySoundPreset = 'soundPreset'; +const _keyWelcomeDismissed = 'welcomeDismissed'; const _keySoundPresetFor = 'soundPresetFor'; const _keyUnreadyOnSwap = 'unreadyOnSwap'; const _keyIdleSitsOut = 'idleSitsOut'; @@ -58,6 +59,8 @@ class AppSettings { /// The sound set (`SoundPreset` name) every sound uses unless /// [soundPresetFor] picks another one for it. final soundPreset = signal('cosmic'); + /// The offline welcome band's × was pressed: never show it again. + final welcomeDismissed = signal(false); /// `SfxKind` name -> `SoundPreset` name, per-sound overrides. final soundPresetFor = signal>(const {}); @@ -110,6 +113,8 @@ class AppSettings { if (vol is num) soundVolume.value = vol.toDouble().clamp(0, 1); final off = box.get(_keySoundsOff); if (off is List) soundsOff.value = Set.unmodifiable(off.whereType()); + final welcomeOff = box.get(_keyWelcomeDismissed); + if (welcomeOff is bool) welcomeDismissed.value = welcomeOff; final soundSet = box.get(_keySoundPreset); if (soundSet is String) soundPreset.value = soundSet; final per = box.get(_keySoundPresetFor); @@ -186,6 +191,11 @@ class AppSettings { _put(_keySoundsOff, soundsOff.value.toList()); } + void dismissWelcome() { + welcomeDismissed.value = true; + _put(_keyWelcomeDismissed, true); + } + void setSoundPreset(String preset) { soundPreset.value = preset; _put(_keySoundPreset, preset); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 682fbb5..e369b7f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -733,7 +733,9 @@ always scrolls the page). Space never presses a focused button bytes). Online, a name change (once typing settles) or picture change goes out through `Profile.push` (`coordinator.updateProfile`). - **Welcome** (`welcome.dart`): offline the Library opens with - `WelcomeBand` over the source switch (sizes away when online): the app + `WelcomeBand` over the source switch, only for newcomers (no Jellyfin + account, no recent server, not dismissed with "Don't show again"; + sizes away when online): the app icon, Starsync and "Watch together, in sync", then three `WayCard`s, Join a friend first (filled), Host, Watch solo (opens Files, or Jellyfin when linked); beside the name from 940 px, under it below,