Add avatar-menu navigation style and inline Android search bar
- New Settings > Appearance > "Navigation menu" toggle (SettingsController.navMenuStyle, NooNavMenuStyle.drawer/avatarMenu): the hamburger-opens-a-Drawer pattern stays the default, or the avatar button opens a full-width dropdown (showAvatarMenu) instead, holding the same hidden-tabs + Settings content the drawer does. Removes one of the two top-corner targets the shell asks a thumb to reach, with no permanent chrome added. - NooTopBar gains androidTitleReplacement so Android's top bar can swap its title for an inline search-field-styled launcher (ShellSearchLauncher) when search isn't in the bottom bar, instead of a bare search icon beside it. - ShellAvatarButton's onTap/label are now overridable so AppTopBar can repoint it per the new setting without this file depending on avatar_menu.dart (which already depends on it for shared helpers). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -423,6 +423,7 @@ class _MainShellViewState extends State<MainShellView> {
|
||||
style: navStyle,
|
||||
tab: tab,
|
||||
searchInBottomBar: showBottomBarSearch,
|
||||
navMenuStyle: settings.navMenuStyle,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ class SettingsController extends ChangeNotifier {
|
||||
static const _prefSwipeLeftAction = 'ui_swipe_left_action';
|
||||
static const _prefSwipeRightAction = 'ui_swipe_right_action';
|
||||
static const _prefBottomBarStyle = 'ui_bottom_bar_style';
|
||||
static const _prefNavMenuStyle = 'ui_nav_menu_style';
|
||||
static const _prefSearchInBottomBar = 'ui_search_in_bottom_bar';
|
||||
static const _prefAmoledDark = 'ui_amoled_dark';
|
||||
static const _prefMediaProgressBarStyle = 'ui_media_progress_bar_style';
|
||||
@@ -43,6 +44,7 @@ class SettingsController extends ChangeNotifier {
|
||||
Color _seedColor = AppTheme.defaultAccent;
|
||||
ThemeMode _themeMode = ThemeMode.system;
|
||||
NooBottomBarStyle _bottomBarStyle = NooBottomBarStyle.attached;
|
||||
NooNavMenuStyle _navMenuStyle = NooNavMenuStyle.drawer;
|
||||
bool _searchInBottomBar = false;
|
||||
bool _useDynamicColor = true;
|
||||
bool _amoledDark = false;
|
||||
@@ -86,6 +88,7 @@ class SettingsController extends ChangeNotifier {
|
||||
Color get seedColor => _seedColor;
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
NooBottomBarStyle get bottomBarStyle => _bottomBarStyle;
|
||||
NooNavMenuStyle get navMenuStyle => _navMenuStyle;
|
||||
bool get searchInBottomBar => _searchInBottomBar;
|
||||
bool get useDynamicColor => _useDynamicColor;
|
||||
bool get amoledDark => _amoledDark;
|
||||
@@ -137,6 +140,13 @@ class SettingsController extends ChangeNotifier {
|
||||
orElse: () => _bottomBarStyle,
|
||||
);
|
||||
}
|
||||
final navMenuStyleName = prefs.getString(_prefNavMenuStyle);
|
||||
if (navMenuStyleName != null) {
|
||||
_navMenuStyle = NooNavMenuStyle.values.firstWhere(
|
||||
(s) => s.name == navMenuStyleName,
|
||||
orElse: () => _navMenuStyle,
|
||||
);
|
||||
}
|
||||
_searchInBottomBar =
|
||||
prefs.getBool(_prefSearchInBottomBar) ?? _searchInBottomBar;
|
||||
_useDynamicColor =
|
||||
@@ -287,6 +297,13 @@ class SettingsController extends ChangeNotifier {
|
||||
_prefsFuture.then((p) => p.setString(_prefBottomBarStyle, style.name));
|
||||
}
|
||||
|
||||
void setNavMenuStyle(NooNavMenuStyle style) {
|
||||
if (_navMenuStyle == style) return;
|
||||
_navMenuStyle = style;
|
||||
notifyListeners();
|
||||
_prefsFuture.then((p) => p.setString(_prefNavMenuStyle, style.name));
|
||||
}
|
||||
|
||||
/// Turning this on lowers [maxVisibleTabs] by one, so re-enforces the cap
|
||||
/// immediately in case the user already has a full 5 regular tabs pinned
|
||||
/// - same cleanup [_enforceMaxVisibleTabs] already does for a fresh
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import 'avatar_menu.dart';
|
||||
import 'create_menu.dart';
|
||||
import 'noo/nav/noo_top_bar.dart';
|
||||
import 'shell/shell_common.dart';
|
||||
@@ -13,8 +14,11 @@ import 'shell/shell_common.dart';
|
||||
/// - iOS: `menu` leading, `plus` (Files only - no tab besides Files has a
|
||||
/// create/upload flow) and the account avatar trailing, then the large
|
||||
/// title, then an inline search field that opens [SearchView].
|
||||
/// - Android: `menu`, title, `search` and avatar actions. Upload has no
|
||||
/// top-bar icon here - it's the [NooFab] `MainShellView` shows instead.
|
||||
/// - Android: `menu`, title, avatar actions. Upload has no top-bar icon
|
||||
/// here - it's the [NooFab] `MainShellView` shows instead. When search
|
||||
/// isn't in the bottom bar, the title itself is replaced by an inline
|
||||
/// search-field-styled launcher (`androidTitleReplacement`) rather than
|
||||
/// a bare search icon sitting beside it.
|
||||
///
|
||||
/// [style] is resolved by the caller (`NooLayout.navStyle`) rather than
|
||||
/// read from context here, since [preferredSize] is a plain getter with no
|
||||
@@ -22,17 +26,24 @@ import 'shell/shell_common.dart';
|
||||
/// [searchInBottomBar] is [SettingsController.searchInBottomBar], passed in
|
||||
/// for the same reason: it hides this bar's own search field/icon so
|
||||
/// there's only ever one search entry point on screen once the bottom bar
|
||||
/// has taken it over.
|
||||
/// has taken it over. [navMenuStyle] is
|
||||
/// [SettingsController.navMenuStyle]: [NooNavMenuStyle.drawer] wires the
|
||||
/// `menu` icon to `Scaffold.of(context).openDrawer()` as before;
|
||||
/// [NooNavMenuStyle.avatarMenu] drops that icon (`onMenu: null`) and makes
|
||||
/// the avatar button open `showAvatarMenu` instead of the account
|
||||
/// switcher.
|
||||
class AppTopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final NooNavStyle style;
|
||||
final AppTab tab;
|
||||
final bool searchInBottomBar;
|
||||
final NooNavMenuStyle navMenuStyle;
|
||||
|
||||
const AppTopBar({
|
||||
super.key,
|
||||
required this.style,
|
||||
required this.tab,
|
||||
this.searchInBottomBar = false,
|
||||
this.navMenuStyle = NooNavMenuStyle.drawer,
|
||||
});
|
||||
|
||||
bool get _ios => style == NooNavStyle.ios;
|
||||
@@ -53,21 +64,31 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
return NooTopBar(
|
||||
style: style,
|
||||
title: tab.label,
|
||||
onMenu: () => Scaffold.of(context).openDrawer(),
|
||||
onMenu: navMenuStyle == NooNavMenuStyle.drawer
|
||||
? () => Scaffold.of(context).openDrawer()
|
||||
: null,
|
||||
// Android has no large title to put a second search row under the
|
||||
// way iOS does (`search:` below) - an inline bar replaces the title
|
||||
// itself instead of sitting beside it as a plain icon action.
|
||||
androidTitleReplacement: !_ios && _showSearch
|
||||
? const ShellSearchLauncher()
|
||||
: null,
|
||||
actions: [
|
||||
if (!_ios && _showSearch)
|
||||
NooTopBarButton(
|
||||
icon: LucideIcons.search,
|
||||
tooltip: 'Search',
|
||||
onTap: () => openSearch(context),
|
||||
),
|
||||
if (_ios && tab == AppTab.files)
|
||||
NooTopBarButton(
|
||||
icon: LucideIcons.plus,
|
||||
tooltip: 'Create',
|
||||
onTap: () => showCreateMenu(context),
|
||||
),
|
||||
ShellAvatarButton(hitBox: _ios ? 44 : 48),
|
||||
ShellAvatarButton(
|
||||
hitBox: _ios ? 44 : 48,
|
||||
onTap: navMenuStyle == NooNavMenuStyle.avatarMenu
|
||||
? () => showAvatarMenu(context)
|
||||
: null,
|
||||
label: navMenuStyle == NooNavMenuStyle.avatarMenu
|
||||
? 'Menu'
|
||||
: 'Accounts',
|
||||
),
|
||||
],
|
||||
search: _ios && _showSearch ? const ShellSearchLauncher(ios: true) : null,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../providers/trash_controller.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import 'noo/core/noo_avatar.dart';
|
||||
import 'noo/core/noo_badge.dart';
|
||||
import 'noo/lists/noo_settings_row.dart';
|
||||
import 'noo/nav/noo_top_bar.dart';
|
||||
import 'noo/noo_layout.dart';
|
||||
import 'shell/shell_common.dart';
|
||||
|
||||
/// The dropdown [ShellAvatarButton] opens when
|
||||
/// [SettingsController.navMenuStyle] is [NooNavMenuStyle.avatarMenu] -
|
||||
/// everything [AppDrawer] holds beyond its storage bar (hidden tabs,
|
||||
/// Settings), reached from the avatar instead of a hamburger-triggered
|
||||
/// left `Drawer`. Built with `showGeneralDialog` rather than a `Drawer`/
|
||||
/// `showNooSheet`/`showNooDialog` - this needs a transparent (non-dimming)
|
||||
/// barrier and a card anchored under the top bar rather than a modal
|
||||
/// sheet/dialog, and there's no existing anchored-popup primitive in this
|
||||
/// app to reuse (`PopupMenuButton`'s own width doesn't stretch to the full
|
||||
/// content column the way this needs to).
|
||||
Future<void> showAvatarMenu(BuildContext context) {
|
||||
final navStyle = NooLayout.navStyle(context);
|
||||
return showGeneralDialog<void>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
barrierLabel: 'Close menu',
|
||||
transitionDuration: NooMotion.fast,
|
||||
pageBuilder: (context, _, _) => _AvatarMenuContent(navStyle: navStyle),
|
||||
transitionBuilder: (context, animation, _, child) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
alignment: Alignment.topRight,
|
||||
scale: Tween<double>(
|
||||
begin: 0.96,
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(parent: animation, curve: NooMotion.ease)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _AvatarMenuContent extends StatelessWidget {
|
||||
final NooNavStyle navStyle;
|
||||
|
||||
const _AvatarMenuContent({required this.navStyle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.nooColors;
|
||||
final session = context.watch<SessionController>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
final trashCount = context.watch<TrashController>().items.length;
|
||||
final hiddenTabs = settings.tabOrder
|
||||
.where((t) => settings.hiddenTabs.contains(t))
|
||||
.toList();
|
||||
// The row/avatar it opens from is always this tall + the status bar
|
||||
// above it (the button that opens this can't be tapped while its own
|
||||
// top bar is scrolled away, so it's always on-screen at this exact
|
||||
// position when that happens) - matches `NooTopBar.preferredSize`
|
||||
// exactly rather than a guessed constant.
|
||||
final topBarHeight = NooTopBar(
|
||||
style: navStyle,
|
||||
title: '',
|
||||
).preferredSize.height;
|
||||
|
||||
void closeAndOpenSettings() {
|
||||
Navigator.pop(context);
|
||||
openSettings(context);
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
NooSpace.md,
|
||||
topBarHeight,
|
||||
NooSpace.md,
|
||||
0,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
border: Border.all(color: colors.line),
|
||||
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||
boxShadow: const [nooDialogShadow],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Avatar on the right, name/host on the left - mirrors the
|
||||
// top bar's own right-aligned avatar exactly, so this
|
||||
// header reads as a continuation of the button that
|
||||
// opened it rather than a disconnected card.
|
||||
InkWell(
|
||||
onTap: closeAndOpenSettings,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(NooSpace.md),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
session.username,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: NooText.bodyL.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.fg1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
serverHost(session.serverUrl),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: NooText.meta.copyWith(
|
||||
color: colors.fg3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
NooAvatar(
|
||||
initials: accountInitial(session.username),
|
||||
current: true,
|
||||
size: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hiddenTabs.isNotEmpty) ...[
|
||||
Divider(height: 1, color: colors.line),
|
||||
for (final tab in hiddenTabs)
|
||||
NooSettingsRow(
|
||||
icon: tab.icon,
|
||||
label: Text(tab.label),
|
||||
trailing: tab == AppTab.trash && trashCount > 0
|
||||
? NooBadge(
|
||||
tone: NooBadgeTone.accent,
|
||||
child: Text('$trashCount'),
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
settings.requestTab(tab);
|
||||
},
|
||||
),
|
||||
],
|
||||
Divider(height: 1, color: colors.line),
|
||||
NooSettingsRow(
|
||||
icon: LucideIcons.settings,
|
||||
label: const Text('Settings'),
|
||||
onTap: closeAndOpenSettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,20 @@ enum NooNavStyle {
|
||||
/// trailing padding to clear the bar (see `NooBottomBar.rowHeight`).
|
||||
enum NooBottomBarStyle { attached, floating }
|
||||
|
||||
/// Which widget opens the shell's navigation menu (hidden tabs + Settings)
|
||||
/// on mobile - user-configurable in Settings (Appearance), same precedent
|
||||
/// as [NooBottomBarStyle]. [drawer] is the original pattern: a `menu`
|
||||
/// icon, top-left of the top bar, opens a left-edge `Drawer`. [avatarMenu]
|
||||
/// drops that icon entirely and repurposes the avatar button - already
|
||||
/// sitting top-right, already a tap target every user already knows - as
|
||||
/// the one entry point instead, opening a dropdown anchored below it (see
|
||||
/// `showAvatarMenu` in `widgets/avatar_menu.dart`) rather than a drawer
|
||||
/// sliding from the opposite edge. Applies on both platforms, since
|
||||
/// [NooTopBar]'s `onMenu`/avatar wiring is shared chrome, not
|
||||
/// Android-specific - only [AppDrawer]'s left-`Drawer` convention is being
|
||||
/// offered an alternative, not anything platform-only.
|
||||
enum NooNavMenuStyle { drawer, avatarMenu }
|
||||
|
||||
/// One destination in a [NooBottomBar]. Data-agnostic on purpose - the app
|
||||
/// maps its own tab model (e.g. `AppTab`) onto these.
|
||||
@immutable
|
||||
|
||||
@@ -56,6 +56,15 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// iOS only: the search field shown below the large title.
|
||||
final Widget? search;
|
||||
|
||||
/// Android only: replaces the plain title text with an arbitrary widget
|
||||
/// (e.g. a search-field-styled launcher) in that same flexible middle
|
||||
/// slot, between [leading]/the menu button and [actions]. iOS already
|
||||
/// has a dedicated [search] slot below its large title for this; Android
|
||||
/// has no large title to put a second row under, so a caller that wants
|
||||
/// an inline search bar there replaces the title with one instead of
|
||||
/// adding a row.
|
||||
final Widget? androidTitleReplacement;
|
||||
|
||||
/// Defaults to `bg` - the top bar sits on the screen background.
|
||||
final Color? backgroundColor;
|
||||
|
||||
@@ -67,6 +76,7 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.onMenu,
|
||||
this.actions = const [],
|
||||
this.search,
|
||||
this.androidTitleReplacement,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@@ -179,12 +189,14 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: [
|
||||
?lead,
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: NooText.title.copyWith(height: 1, color: colors.fg1),
|
||||
),
|
||||
child:
|
||||
androidTitleReplacement ??
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: NooText.title.copyWith(height: 1, color: colors.fg1),
|
||||
),
|
||||
),
|
||||
...actions,
|
||||
],
|
||||
|
||||
@@ -51,6 +51,7 @@ class SettingsAppearanceSection extends StatelessWidget {
|
||||
children: [
|
||||
_ThemeRow(settings: settings),
|
||||
_BottomBarStyleRow(settings: settings),
|
||||
_NavMenuStyleRow(settings: settings),
|
||||
NooSettingsRow(
|
||||
icon: LucideIcons.search,
|
||||
label: const Text('Search in bottom bar'),
|
||||
@@ -230,6 +231,66 @@ class _BottomBarStyleRow extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// "Navigation menu" (Settings → Appearance): which widget opens hidden
|
||||
/// tabs + Settings - the original `menu` icon that opens a left `Drawer`,
|
||||
/// or the avatar button instead (opens `showAvatarMenu`, a dropdown
|
||||
/// anchored under it) - see [NooNavMenuStyle]'s own doc comment for the
|
||||
/// full reachability rationale (dropping the hamburger removes one of the
|
||||
/// two top-corner targets the shell asks a thumb to reach, rather than
|
||||
/// adding a third kind of chrome like a side rail would).
|
||||
class _NavMenuStyleRow extends StatelessWidget {
|
||||
final SettingsController settings;
|
||||
|
||||
const _NavMenuStyleRow({required this.settings});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.nooColors;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: NooSpace.md,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.panelLeft, size: 20, color: colors.fg2),
|
||||
const SizedBox(width: 14),
|
||||
Text(
|
||||
'Navigation menu',
|
||||
style: NooText.bodyL.copyWith(color: colors.fg1),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NooSegmentedControl<NooNavMenuStyle>(
|
||||
fill: true,
|
||||
value: settings.navMenuStyle,
|
||||
onChanged: settings.setNavMenuStyle,
|
||||
options: const [
|
||||
NooSegmentOption(
|
||||
value: NooNavMenuStyle.drawer,
|
||||
icon: LucideIcons.menu,
|
||||
label: 'Hamburger',
|
||||
),
|
||||
NooSegmentOption(
|
||||
value: NooNavMenuStyle.avatarMenu,
|
||||
icon: LucideIcons.userRound,
|
||||
label: 'Avatar',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The current accent-color choice, shown as a small filled circle in the
|
||||
/// "Accent color" row - a wallpaper icon when following the OS's dynamic
|
||||
/// color instead of a fixed swatch.
|
||||
|
||||
@@ -212,13 +212,31 @@ class _AccountSwitchRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// The current user's avatar in the mobile top bar. Tap opens the accounts
|
||||
/// sheet ([showAccountSwitcher]); swiping up/down on it cycles to the
|
||||
/// next/previous saved account immediately, with no confirmation - a
|
||||
/// shortcut alongside the full switcher.
|
||||
/// sheet ([showAccountSwitcher]) by default - or [onTap], when the caller
|
||||
/// overrides it (`AppTopBar` does this for
|
||||
/// `SettingsController.navMenuStyle`'s `avatarMenu` option, which
|
||||
/// repurposes this same button to open `showAvatarMenu` instead; avoids
|
||||
/// this file importing `widgets/avatar_menu.dart`, which itself imports
|
||||
/// this file for `openSettings`/`accountInitial`/`serverHost`). Swiping
|
||||
/// up/down on it cycles to the next/previous saved account immediately,
|
||||
/// with no confirmation, regardless of [onTap] - a shortcut that stays
|
||||
/// available either way.
|
||||
class ShellAvatarButton extends StatelessWidget {
|
||||
final double hitBox;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ShellAvatarButton({super.key, this.hitBox = 48});
|
||||
/// Describes whatever [onTap] actually does - defaults to "Accounts"
|
||||
/// (the account switcher), but a caller overriding [onTap] should
|
||||
/// override this too so the tooltip/semantics describe the real action
|
||||
/// instead of a stale default.
|
||||
final String label;
|
||||
|
||||
const ShellAvatarButton({
|
||||
super.key,
|
||||
this.hitBox = 48,
|
||||
this.onTap,
|
||||
this.label = 'Accounts',
|
||||
});
|
||||
|
||||
Future<void> _handleVerticalSwipe(
|
||||
BuildContext context,
|
||||
@@ -246,17 +264,17 @@ class ShellAvatarButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final session = context.watch<SessionController>();
|
||||
return Tooltip(
|
||||
message: 'Accounts',
|
||||
message: label,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Accounts',
|
||||
label: label,
|
||||
child: GestureDetector(
|
||||
onVerticalDragEnd: (details) =>
|
||||
_handleVerticalSwipe(context, details),
|
||||
child: SizedBox.square(
|
||||
dimension: hitBox,
|
||||
child: InkResponse(
|
||||
onTap: () => showAccountSwitcher(context),
|
||||
onTap: onTap ?? () => showAccountSwitcher(context),
|
||||
radius: hitBox / 2,
|
||||
child: Center(
|
||||
child: NooAvatar(
|
||||
|
||||
Reference in New Issue
Block a user