mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
isolate home workspace navigation
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
@@ -26,6 +27,8 @@ import '../profile/profile_avatar.dart';
|
||||
import '../profile/profile_provider.dart';
|
||||
import '../profile/presence_cache_provider.dart';
|
||||
import '../profile/user_cache_provider.dart';
|
||||
import '../profile/user_profile.dart';
|
||||
import '../profile/user_status_provider.dart';
|
||||
import '../pairing/pairing_page.dart';
|
||||
import '../pairing/pairing_provider.dart';
|
||||
import 'channel.dart';
|
||||
@@ -53,6 +56,7 @@ part 'channels_page/skeleton.dart';
|
||||
part 'channels_page/community.dart';
|
||||
part 'channels_page/quick_actions.dart';
|
||||
part 'channels_page/quick_actions_launcher.dart';
|
||||
part 'channels_page/wide_navigation.dart';
|
||||
|
||||
enum _QuickAction { createChannel, newDm }
|
||||
|
||||
|
||||
+79
-40
@@ -1,16 +1,42 @@
|
||||
part of '../home_page.dart';
|
||||
part of '../channels_page.dart';
|
||||
|
||||
class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
const _wideNavigationWidth = 280.0;
|
||||
const _wideNavigationIconSize = 20.0;
|
||||
const _wideNavigationDmAvatarSize = 24.0;
|
||||
const _wideNavigationPrimaryRowHeight = 52.0;
|
||||
const _wideNavigationChannelRowHeight = 48.0;
|
||||
const _wideNavigationLabelGap = Grid.twelve - Grid.quarter;
|
||||
const _wideNavigationDmLabelGap = _wideNavigationLabelGap - Grid.quarter;
|
||||
const _wideNavigationIdentityAvatarInset =
|
||||
Grid.xs + Grid.half - (Grid.quarter / 2);
|
||||
const _wideNavigationIdentityLabelGap = Grid.half + (Grid.quarter / 2);
|
||||
|
||||
class WideNavigationDestination {
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final String label;
|
||||
|
||||
const WideNavigationDestination({
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
|
||||
class WideChannelsNavigation extends HookConsumerWidget {
|
||||
final int? selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
final ValueChanged<Channel> onChannelSelected;
|
||||
final ValueChanged<String> onChannelSelected;
|
||||
final String? selectedChannelId;
|
||||
final VoidCallback onProfileSelected;
|
||||
final bool isCommunitySwitching;
|
||||
final ValueChanged<String?> onCommunitySwitchStart;
|
||||
final List<_HomeDestination> destinations;
|
||||
final String? pendingCommunityId;
|
||||
final VoidCallback onCommunitySwitchComplete;
|
||||
final List<WideNavigationDestination> destinations;
|
||||
|
||||
const _WideNavigationSidebar({
|
||||
const WideChannelsNavigation({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
required this.onChannelSelected,
|
||||
@@ -18,6 +44,8 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
required this.onProfileSelected,
|
||||
required this.isCommunitySwitching,
|
||||
required this.onCommunitySwitchStart,
|
||||
required this.pendingCommunityId,
|
||||
required this.onCommunitySwitchComplete,
|
||||
required this.destinations,
|
||||
});
|
||||
|
||||
@@ -32,6 +60,17 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
final currentPubkey = ref.watch(profileProvider).value?.pubkey;
|
||||
final profiles = ref.watch(userCacheProvider);
|
||||
final channelsAsync = ref.watch(channelsProvider);
|
||||
final activeCommunityId = ref.watch(activeCommunityProvider).value?.id;
|
||||
useEffect(() {
|
||||
if (pendingCommunityId != activeCommunityId || channelsAsync.isLoading) {
|
||||
return null;
|
||||
}
|
||||
final timer = Timer(
|
||||
const Duration(milliseconds: 250),
|
||||
onCommunitySwitchComplete,
|
||||
);
|
||||
return timer.cancel;
|
||||
}, [pendingCommunityId, activeCommunityId, channelsAsync.isLoading]);
|
||||
final channels = (channelsAsync.asData?.value ?? const <Channel>[])
|
||||
.where((channel) => !channel.isArchived)
|
||||
.toList();
|
||||
@@ -68,7 +107,7 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
|
||||
Future<void> createChannel() async {
|
||||
final channel = await showCreateChannelSheet(context);
|
||||
if (channel != null) onChannelSelected(channel);
|
||||
if (channel != null) onChannelSelected(channel.id);
|
||||
}
|
||||
|
||||
Future<void> createDirectMessage() async {
|
||||
@@ -76,12 +115,12 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
context,
|
||||
currentPubkey: currentPubkey,
|
||||
);
|
||||
if (channel != null) onChannelSelected(channel);
|
||||
if (channel != null) onChannelSelected(channel.id);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
key: const Key('wide-navigation-sidebar'),
|
||||
width: HomePage._wideNavigationWidth,
|
||||
width: _wideNavigationWidth,
|
||||
child: DecoratedBox(
|
||||
key: const Key('wide-navigation-sidebar-background'),
|
||||
decoration: BoxDecoration(
|
||||
@@ -109,7 +148,7 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: SkeletonReveal(
|
||||
loading: isCommunitySwitching,
|
||||
skeleton: const _WideNavigationSidebarSkeleton(
|
||||
skeleton: const SizedBox(
|
||||
key: Key('wide-navigation-community-switch-skeleton'),
|
||||
),
|
||||
content: Column(
|
||||
@@ -158,7 +197,7 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
profiles: profiles,
|
||||
),
|
||||
selected: channel.id == selectedChannelId,
|
||||
onTap: () => onChannelSelected(channel),
|
||||
onTap: () => onChannelSelected(channel.id),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
_WideNavigationSectionLabel(
|
||||
@@ -182,7 +221,7 @@ class _WideNavigationSidebar extends HookConsumerWidget {
|
||||
profiles: profiles,
|
||||
),
|
||||
selected: channel.id == selectedChannelId,
|
||||
onTap: () => onChannelSelected(channel),
|
||||
onTap: () => onChannelSelected(channel.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -399,7 +438,7 @@ class _WideNavigationCommunitySwitcher extends HookConsumerWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: HomePage._wideNavigationIdentityAvatarInset,
|
||||
left: _wideNavigationIdentityAvatarInset,
|
||||
right: Grid.sm,
|
||||
),
|
||||
child: Row(
|
||||
@@ -429,9 +468,7 @@ class _WideNavigationCommunitySwitcher extends HookConsumerWidget {
|
||||
},
|
||||
onVerticalDragCancel: resetSwipe,
|
||||
),
|
||||
const SizedBox(
|
||||
width: HomePage._wideNavigationIdentityLabelGap,
|
||||
),
|
||||
const SizedBox(width: _wideNavigationIdentityLabelGap),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 192),
|
||||
// The avatar motion carries the change. Keeping the
|
||||
@@ -599,15 +636,13 @@ class _WideNavigationProfile extends ConsumerWidget {
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: HomePage._wideNavigationIdentityAvatarInset,
|
||||
left: _wideNavigationIdentityAvatarInset,
|
||||
right: Grid.half,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const ProfileAvatar(),
|
||||
const SizedBox(
|
||||
width: HomePage._wideNavigationIdentityLabelGap,
|
||||
),
|
||||
const SizedBox(width: _wideNavigationIdentityLabelGap),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -692,7 +727,7 @@ class _WideChannelDestination extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: Grid.quarter),
|
||||
child: SizedBox(
|
||||
height: HomePage._wideNavigationChannelRowHeight,
|
||||
height: _wideNavigationChannelRowHeight,
|
||||
child: Material(
|
||||
color: selected ? palette.activeSurface : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(Radii.sm),
|
||||
@@ -704,16 +739,13 @@ class _WideChannelDestination extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: channel.isDm
|
||||
? HomePage._wideNavigationDmAvatarSize
|
||||
: HomePage._tabIconSize,
|
||||
width: channel.isDm ? _wideNavigationDmAvatarSize : 22,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: channel.isDm
|
||||
? AvatarImage(
|
||||
imageUrl: directMessageProfile?.avatarUrl,
|
||||
radius:
|
||||
HomePage._wideNavigationDmAvatarSize / 2,
|
||||
radius: _wideNavigationDmAvatarSize / 2,
|
||||
backgroundColor:
|
||||
context.colors.primaryContainer,
|
||||
fallback: Text(
|
||||
@@ -727,14 +759,14 @@ class _WideChannelDestination extends StatelessWidget {
|
||||
: Icon(
|
||||
icon,
|
||||
color: foregroundColor,
|
||||
size: HomePage._wideNavigationIconSize,
|
||||
size: _wideNavigationIconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: channel.isDm
|
||||
? HomePage._wideNavigationDmLabelGap
|
||||
: HomePage._wideNavigationLabelGap,
|
||||
? _wideNavigationDmLabelGap
|
||||
: _wideNavigationLabelGap,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -760,22 +792,29 @@ class _WideChannelDestination extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _WideChannelContent extends HookWidget {
|
||||
final Channel channel;
|
||||
class WideChannelContent extends HookConsumerWidget {
|
||||
final String channelId;
|
||||
final VoidCallback onChannelLeft;
|
||||
|
||||
const _WideChannelContent({
|
||||
required this.channel,
|
||||
const WideChannelContent({
|
||||
super.key,
|
||||
required this.channelId,
|
||||
required this.onChannelLeft,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final channels = ref.watch(channelsProvider).asData?.value;
|
||||
final channel = channels?.cast<Channel?>().firstWhere(
|
||||
(channel) => channel?.id == channelId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (channel == null) return const SizedBox.shrink();
|
||||
final navigatorKey = useMemoized(GlobalKey<NavigatorState>.new, [
|
||||
channel.id,
|
||||
channelId,
|
||||
]);
|
||||
return NavigatorPopHandler(
|
||||
key: ValueKey('wide-channel-detail-${channel.id}'),
|
||||
key: ValueKey('wide-channel-detail-$channelId'),
|
||||
onPopWithResult: (_) => navigatorKey.currentState?.maybePop(),
|
||||
child: Navigator(
|
||||
key: navigatorKey,
|
||||
@@ -789,7 +828,7 @@ class _WideChannelContent extends HookWidget {
|
||||
}
|
||||
|
||||
class _WideNavigationDestination extends StatelessWidget {
|
||||
final _HomeDestination destination;
|
||||
final WideNavigationDestination destination;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -817,7 +856,7 @@ class _WideNavigationDestination extends StatelessWidget {
|
||||
// channel list directly below it.
|
||||
padding: const EdgeInsets.only(bottom: Grid.quarter),
|
||||
child: SizedBox(
|
||||
height: HomePage._wideNavigationPrimaryRowHeight,
|
||||
height: _wideNavigationPrimaryRowHeight,
|
||||
child: Material(
|
||||
color: selected ? palette.activeSurface : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(Radii.md),
|
||||
@@ -829,7 +868,7 @@ class _WideNavigationDestination extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: HomePage._tabIconSize,
|
||||
width: 22,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Icon(
|
||||
@@ -838,11 +877,11 @@ class _WideNavigationDestination extends StatelessWidget {
|
||||
// background, and label weight instead.
|
||||
destination.selectedIcon,
|
||||
color: foregroundColor,
|
||||
size: HomePage._wideNavigationIconSize,
|
||||
size: _wideNavigationIconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: HomePage._wideNavigationLabelGap),
|
||||
const SizedBox(width: _wideNavigationLabelGap),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
@@ -7,27 +7,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../shared/community/community.dart';
|
||||
import '../../shared/community/community_icon_provider.dart';
|
||||
import '../../shared/community/community_provider.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/widgets/mobile_tab_footer_backdrop.dart';
|
||||
import '../../shared/widgets/skeleton.dart';
|
||||
import '../activity/activity_page.dart';
|
||||
import '../channels/channel.dart';
|
||||
import '../channels/channel_detail_page.dart';
|
||||
import '../channels/channels_page.dart';
|
||||
import '../channels/channels_provider.dart';
|
||||
import '../channels/dm_channel_labels.dart';
|
||||
import '../profile/profile_avatar.dart';
|
||||
import '../profile/profile_provider.dart';
|
||||
import '../profile/user_cache_provider.dart';
|
||||
import '../profile/user_profile.dart';
|
||||
import '../profile/user_status_provider.dart';
|
||||
import '../search/search_page.dart';
|
||||
|
||||
part 'home_page/wide_navigation.dart';
|
||||
part 'home_page/wide_navigation_skeletons.dart';
|
||||
|
||||
class HomePage extends HookConsumerWidget {
|
||||
@@ -44,36 +31,24 @@ class HomePage extends HookConsumerWidget {
|
||||
static const double _tabBarHorizontalMargin = Grid.gutter;
|
||||
static const double _tabDestinationHorizontalPadding = Grid.sm;
|
||||
static const double _tabIconSize = 22;
|
||||
static const double _wideNavigationIconSize = 20;
|
||||
static const double _wideNavigationDmAvatarSize = 24;
|
||||
static const double _fabClearance = _tabBarHeight + _tabBarBottomGap;
|
||||
static const Duration _tabIconWeightDuration = Duration(milliseconds: 120);
|
||||
static const double _wideNavigationBreakpoint = 840;
|
||||
static const double _wideNavigationWidth = 280;
|
||||
static const double _wideContentInset = Grid.half + Grid.quarter;
|
||||
static const double _wideContentRadius = 24;
|
||||
static const double _wideNavigationPrimaryRowHeight = 52;
|
||||
static const double _wideNavigationChannelRowHeight = 48;
|
||||
static const double _wideNavigationLabelGap = Grid.twelve - Grid.quarter;
|
||||
static const double _wideNavigationDmLabelGap =
|
||||
_wideNavigationLabelGap - Grid.quarter;
|
||||
static const double _wideNavigationIdentityAvatarInset =
|
||||
Grid.xs + Grid.half - (Grid.quarter / 2);
|
||||
static const double _wideNavigationIdentityLabelGap =
|
||||
Grid.half + (Grid.quarter / 2);
|
||||
|
||||
static const _destinations = [
|
||||
_HomeDestination(
|
||||
WideNavigationDestination(
|
||||
icon: LucideIcons.house300,
|
||||
selectedIcon: LucideIcons.house500,
|
||||
label: 'Home',
|
||||
),
|
||||
_HomeDestination(
|
||||
WideNavigationDestination(
|
||||
icon: LucideIcons.inbox300,
|
||||
selectedIcon: LucideIcons.inbox500,
|
||||
label: 'Activity',
|
||||
),
|
||||
_HomeDestination(
|
||||
WideNavigationDestination(
|
||||
icon: LucideIcons.search300,
|
||||
selectedIcon: LucideIcons.search500,
|
||||
label: 'Search',
|
||||
@@ -87,7 +62,7 @@ class HomePage extends HookConsumerWidget {
|
||||
// On iPad, the persistent sidebar replaces the phone's Home tab. Start at
|
||||
// Inbox, which is the first top-level destination in that layout.
|
||||
final tabIndex = useState(isWide ? 1 : 0);
|
||||
final selectedChannel = useState<Channel?>(null);
|
||||
final selectedChannelId = useState<String?>(null);
|
||||
final pendingCommunityId = useState<String?>(null);
|
||||
// Clear the tablet selection as soon as the active-community state begins
|
||||
// changing. The channel provider intentionally retains its last result
|
||||
@@ -100,30 +75,9 @@ class HomePage extends HookConsumerWidget {
|
||||
);
|
||||
final selectedChannelCommunityId = useRef<String?>(activeCommunityId);
|
||||
if (selectedChannelCommunityId.value != activeCommunityId) {
|
||||
selectedChannel.value = null;
|
||||
selectedChannelId.value = null;
|
||||
selectedChannelCommunityId.value = activeCommunityId;
|
||||
}
|
||||
final channelsAsync = ref.watch(channelsProvider);
|
||||
final hasActivatedPendingCommunity =
|
||||
pendingCommunityId.value != null &&
|
||||
activeCommunityId == pendingCommunityId.value;
|
||||
useEffect(
|
||||
() {
|
||||
if (!hasActivatedPendingCommunity || channelsAsync.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final timer = Timer(const Duration(milliseconds: 250), () {
|
||||
pendingCommunityId.value = null;
|
||||
});
|
||||
return timer.cancel;
|
||||
},
|
||||
[
|
||||
pendingCommunityId.value,
|
||||
hasActivatedPendingCommunity,
|
||||
channelsAsync.isLoading,
|
||||
],
|
||||
);
|
||||
final isCommunitySwitching = pendingCommunityId.value != null;
|
||||
final systemBottomInset = MediaQuery.paddingOf(context).bottom;
|
||||
final navigationBarWidth = _floatingTabBarWidth(
|
||||
@@ -133,10 +87,10 @@ class HomePage extends HookConsumerWidget {
|
||||
final useSidebarLayout = isWide;
|
||||
|
||||
final pages = [
|
||||
if (isWide && selectedChannel.value != null)
|
||||
_WideChannelContent(
|
||||
channel: selectedChannel.value!,
|
||||
onChannelLeft: () => selectedChannel.value = null,
|
||||
if (isWide && selectedChannelId.value != null)
|
||||
WideChannelContent(
|
||||
channelId: selectedChannelId.value!,
|
||||
onChannelLeft: () => selectedChannelId.value = null,
|
||||
)
|
||||
else
|
||||
ChannelsPage(settingsPageBuilder: settingsPageBuilder),
|
||||
@@ -148,9 +102,9 @@ class HomePage extends HookConsumerWidget {
|
||||
// returning from a nested route must land back in Inbox rather than reveal
|
||||
// the phone's ChannelsPage inside the desktop-style workspace.
|
||||
final wideFallbackToInbox =
|
||||
isWide && selectedChannel.value == null && tabIndex.value == 0;
|
||||
isWide && selectedChannelId.value == null && tabIndex.value == 0;
|
||||
final wideContentIndex = wideFallbackToInbox ? 1 : tabIndex.value;
|
||||
final wideSidebarSelection = selectedChannel.value != null
|
||||
final wideSidebarSelection = selectedChannelId.value != null
|
||||
? null
|
||||
: wideFallbackToInbox
|
||||
? 1
|
||||
@@ -182,11 +136,11 @@ class HomePage extends HookConsumerWidget {
|
||||
child: useSidebarLayout
|
||||
? Row(
|
||||
children: [
|
||||
_WideNavigationSidebar(
|
||||
WideChannelsNavigation(
|
||||
selectedIndex: wideSidebarSelection,
|
||||
onDestinationSelected: (index) {
|
||||
final hadSelectedChannel =
|
||||
selectedChannel.value != null;
|
||||
selectedChannelId.value != null;
|
||||
if (index == tabIndex.value &&
|
||||
!(hadSelectedChannel && index != 0)) {
|
||||
return;
|
||||
@@ -195,18 +149,18 @@ class HomePage extends HookConsumerWidget {
|
||||
// the tablet workspace, so they must also clear
|
||||
// the channel selection that owns the sidebar
|
||||
// highlight.
|
||||
selectedChannel.value = null;
|
||||
selectedChannelId.value = null;
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
tabIndex.value = index;
|
||||
},
|
||||
onChannelSelected: (channel) {
|
||||
selectedChannel.value = channel;
|
||||
onChannelSelected: (channelId) {
|
||||
selectedChannelId.value = channelId;
|
||||
if (tabIndex.value != 0) {
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
}
|
||||
tabIndex.value = 0;
|
||||
},
|
||||
selectedChannelId: selectedChannel.value?.id,
|
||||
selectedChannelId: selectedChannelId.value,
|
||||
onProfileSelected: () => unawaited(
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
@@ -218,6 +172,10 @@ class HomePage extends HookConsumerWidget {
|
||||
onCommunitySwitchStart: (communityId) {
|
||||
pendingCommunityId.value = communityId;
|
||||
},
|
||||
pendingCommunityId: pendingCommunityId.value,
|
||||
onCommunitySwitchComplete: () {
|
||||
pendingCommunityId.value = null;
|
||||
},
|
||||
destinations: _destinations,
|
||||
),
|
||||
Expanded(
|
||||
@@ -380,22 +338,10 @@ MediaQueryData _mediaQueryWithFloatingTabBarClearance(
|
||||
);
|
||||
}
|
||||
|
||||
class _HomeDestination {
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final String label;
|
||||
|
||||
const _HomeDestination({
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
|
||||
class _FloatingTabBar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
final List<_HomeDestination> destinations;
|
||||
final List<WideNavigationDestination> destinations;
|
||||
|
||||
const _FloatingTabBar({
|
||||
required this.selectedIndex,
|
||||
@@ -520,7 +466,7 @@ class _FloatingTabBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _FloatingTabDestination extends StatelessWidget {
|
||||
final _HomeDestination destination;
|
||||
final WideNavigationDestination destination;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
|
||||
@@ -1,96 +1,5 @@
|
||||
part of '../home_page.dart';
|
||||
|
||||
class _WideNavigationSidebarSkeleton extends StatelessWidget {
|
||||
const _WideNavigationSidebarSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget row({double labelWidth = 112, bool avatar = false}) {
|
||||
return SizedBox(
|
||||
height: HomePage._wideNavigationChannelRowHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
SkeletonBar(
|
||||
width: HomePage._wideNavigationIconSize,
|
||||
height: HomePage._wideNavigationIconSize,
|
||||
borderRadius: BorderRadius.circular(
|
||||
avatar ? Radii.full : Radii.sm,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: HomePage._wideNavigationLabelGap),
|
||||
SkeletonBar(width: labelWidth, height: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
row(labelWidth: 64),
|
||||
row(labelWidth: 72),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: Grid.sm, top: Grid.sm),
|
||||
child: SkeletonBar(width: 68, height: 12),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
row(labelWidth: 128),
|
||||
row(labelWidth: 104),
|
||||
row(labelWidth: 144),
|
||||
const SizedBox(height: Grid.xs),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: Grid.sm, top: Grid.sm),
|
||||
child: SkeletonBar(width: 32, height: 12),
|
||||
),
|
||||
const SizedBox(height: Grid.xs),
|
||||
row(labelWidth: 116, avatar: true),
|
||||
row(labelWidth: 92, avatar: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: Grid.xs),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
SkeletonBar(
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: BorderRadius.circular(Radii.full),
|
||||
),
|
||||
const SizedBox(width: Grid.twelve),
|
||||
const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SkeletonBar(width: 104, height: 14),
|
||||
SizedBox(height: Grid.half),
|
||||
SkeletonBar(width: 72, height: 11),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Covers the active tablet pane while the relay changes. The left list and
|
||||
/// right timeline preserve the Inbox/channel spatial structure, so no old
|
||||
/// community text is visible during the handoff.
|
||||
|
||||
Reference in New Issue
Block a user