💄 Redesign explore

This commit is contained in:
2025-09-28 23:07:22 +08:00
parent 5b62f89531
commit 22bf6d1c33
3 changed files with 448 additions and 417 deletions

View File

@@ -51,7 +51,6 @@ Widget notificationIndicatorWidget(
], ],
), ),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
minTileHeight: 40,
contentPadding: EdgeInsets.only(left: 16, right: 15), contentPadding: EdgeInsets.only(left: 16, right: 15),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('notifications'); GoRouter.of(context).pushNamed('notifications');
@@ -99,10 +98,6 @@ class ExploreScreen extends HookConsumerWidget {
final events = ref.watch(eventCalendarProvider(query.value)); final events = ref.watch(eventCalendarProvider(query.value));
final selectedDay = useState(now); final selectedDay = useState(now);
// Function to handle day selection for synchronizing between widgets
void onDaySelected(DateTime day) {
selectedDay.value = day;
}
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
@@ -110,137 +105,123 @@ class ExploreScreen extends HookConsumerWidget {
notificationUnreadCountNotifierProvider, notificationUnreadCountNotifierProvider,
); );
return AppScaffold( final isWide = isWideScreen(context);
isNoBackground: false,
appBar: AppBar( final filterBar = Card(
toolbarHeight: 0, margin: EdgeInsets.zero,
bottom: PreferredSize( child: Row(
preferredSize: const Size.fromHeight(48), children: [
child: Row( Expanded(
children: [ child: TabBar(
Expanded( controller: tabController,
child: TabBar( tabAlignment: TabAlignment.start,
controller: tabController, isScrollable: true,
tabAlignment: TabAlignment.start, dividerColor: Colors.transparent,
isScrollable: true, tabs: [
dividerColor: Colors.transparent, Tab(
tabs: [ icon: Tooltip(
Tab( message: 'explore'.tr(),
icon: Tooltip( child: Icon(
message: 'explore'.tr(), Symbols.explore,
child: Icon( color: Theme.of(context).appBarTheme.foregroundColor!,
Symbols.explore, ),
color: ),
Theme.of( ),
context, Tab(
).appBarTheme.foregroundColor!, icon: Tooltip(
), message: 'exploreFilterSubscriptions'.tr(),
), child: Icon(
), Symbols.subscriptions,
Tab( color: Theme.of(context).appBarTheme.foregroundColor!,
icon: Tooltip( ),
message: 'exploreFilterSubscriptions'.tr(), ),
child: Icon( ),
Symbols.subscriptions, Tab(
color: icon: Tooltip(
Theme.of( message: 'exploreFilterFriends'.tr(),
context, child: Icon(
).appBarTheme.foregroundColor!, Symbols.people,
), color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
Tab( ),
icon: Tooltip( ],
message: 'exploreFilterFriends'.tr(), ),
child: Icon( ),
Symbols.people, IconButton(
color: onPressed: () {
Theme.of( context.pushNamed('articles');
context, },
).appBarTheme.foregroundColor!, icon: Icon(
), Symbols.auto_stories,
), color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
tooltip: 'webArticlesStand'.tr(),
),
PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.category),
const Gap(12),
Text('categories').tr(),
], ],
), ),
), onTap: () {
IconButton( context.pushNamed('postCategories');
onPressed: () {
context.pushNamed('articles');
}, },
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'webArticlesStand'.tr(),
), ),
PopupMenuButton( PopupMenuItem(
itemBuilder: child: Row(
(context) => [ children: [
PopupMenuItem( const Icon(Symbols.label),
child: Row( const Gap(12),
children: [ Text('tags').tr(),
const Icon(Symbols.category), ],
const Gap(12),
Text('categories').tr(),
],
),
onTap: () {
context.pushNamed('postCategories');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
),
],
icon: Icon(
Symbols.action_key,
color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
tooltip: 'search'.tr(), onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
), ),
], ],
) icon: Icon(
.padding(horizontal: 8) Symbols.action_key,
.border( color: Theme.of(context).appBarTheme.foregroundColor!,
bottom: 1 / MediaQuery.of(context).devicePixelRatio, ),
color: Theme.of(context).dividerColor, tooltip: 'search'.tr(),
), ),
), ],
), ).padding(horizontal: 8),
);
return AppScaffold(
isNoBackground: false,
floatingActionButton: InkWell( floatingActionButton: InkWell(
onLongPress: () { onLongPress: () {
context.pushNamed('postCompose', queryParameters: {'type': '1'}).then( context.pushNamed('postCompose', queryParameters: {'type': '1'}).then(
@@ -264,97 +245,20 @@ class ExploreScreen extends HookConsumerWidget {
), ),
), ),
floatingActionButtonLocation: TabbedFabLocation(context), floatingActionButtonLocation: TabbedFabLocation(context),
body: Builder( body:
builder: (context) { isWide
final isWide = isWideScreen(context); ? _buildWideBody(
context,
final bodyView = _buildActivityList( ref,
context, filterBar,
ref, user,
currentFilter.value, notificationCount,
); query,
events,
if (isWide) { selectedDay,
return Row( currentFilter.value,
children: [ )
Flexible(flex: 3, child: bodyView.padding(left: 8)), : _buildNarrowBody(context, ref, filterBar, currentFilter.value),
if (user.value != null)
Flexible(
flex: 2,
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Column(
children: [
CheckInWidget(
margin: EdgeInsets.only(
left: 8,
right: 12,
top: 16,
),
onChecked: () {
ref.invalidate(
eventCalendarProvider(query.value),
);
},
),
if (notificationCount.value != null &&
notificationCount.value! > 0)
notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: EdgeInsets.only(
left: 8,
right: 12,
top: 8,
),
),
PostFeaturedList().padding(
left: 8,
right: 12,
top: 8,
),
FortuneGraphWidget(
margin: EdgeInsets.only(
left: 8,
right: 12,
top: 8,
),
events: events,
constrainWidth: true,
onPointSelected: onDaySelected,
),
],
),
),
),
)
else
Flexible(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome to\nthe Solar Network',
style: Theme.of(context).textTheme.titleLarge,
).bold(),
const Gap(2),
Text(
'Login to explore more!',
style: Theme.of(context).textTheme.bodyLarge,
),
],
).padding(horizontal: 36, vertical: 16),
),
],
);
}
return bodyView;
},
),
); );
} }
@@ -369,23 +273,171 @@ class ExploreScreen extends HookConsumerWidget {
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
return ExtendedRefreshIndicator( return PagingHelperSliverView(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), provider: activityListNotifierProvider(filter),
child: PagingHelperView( futureRefreshable: activityListNotifierProvider(filter).future,
provider: activityListNotifierProvider(filter), notifierRefreshable: activityListNotifierProvider(filter).notifier,
futureRefreshable: activityListNotifierProvider(filter).future, contentBuilder:
notifierRefreshable: activityListNotifierProvider(filter).notifier, (data, widgetCount, endItemView) => _ActivityListView(
contentBuilder: data: data,
(data, widgetCount, endItemView) => Center( widgetCount: widgetCount,
child: _ActivityListView( endItemView: endItemView,
data: data, activitiesNotifier: activitiesNotifier,
widgetCount: widgetCount, isWide: isWide,
endItemView: endItemView, ),
activitiesNotifier: activitiesNotifier, );
contentOnly: isWide || filter != null, }
Widget _buildWideBody(
BuildContext context,
WidgetRef ref,
Widget filterBar,
AsyncValue<dynamic> user,
AsyncValue<int?> notificationCount,
ValueNotifier<EventCalendarQuery> query,
AsyncValue<List<dynamic>> events,
ValueNotifier<DateTime> selectedDay,
String? currentFilter,
) {
final bodyView = _buildActivityList(context, ref, currentFilter);
final activitiesNotifier = ref.watch(
activityListNotifierProvider(currentFilter).notifier,
);
return Row(
spacing: 12,
children: [
Flexible(
flex: 3,
child: ExtendedRefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: CustomScrollView(
slivers: [
const SliverGap(12),
SliverToBoxAdapter(child: filterBar),
const SliverGap(8),
bodyView,
],
),
),
),
if (user.value != null)
Flexible(
flex: 2,
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Column(
spacing: 8,
children: [
CheckInWidget(
margin: EdgeInsets.only(top: 12),
onChecked: () {
ref.invalidate(eventCalendarProvider(query.value));
},
),
if (notificationCount.value != null &&
notificationCount.value! > 0)
notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: EdgeInsets.zero,
),
PostFeaturedList(),
FortuneGraphWidget(
margin: EdgeInsets.zero,
events: events as AsyncValue<List<SnEventCalendarEntry>>,
constrainWidth: true,
onPointSelected: (DateTime day) {
selectedDay.value = day;
},
),
],
),
), ),
), ),
), )
else
Flexible(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome to\nthe Solar Network',
style: Theme.of(context).textTheme.titleLarge,
).bold(),
const Gap(2),
Text(
'Login to explore more!',
style: Theme.of(context).textTheme.bodyLarge,
),
],
).padding(horizontal: 36, vertical: 16),
),
],
).padding(horizontal: 12);
}
Widget _buildNarrowBody(
BuildContext context,
WidgetRef ref,
Widget filterBar,
String? currentFilter,
) {
final user = ref.watch(userInfoProvider);
final notificationCount = ref.watch(
notificationUnreadCountNotifierProvider,
);
final activitiesNotifier = ref.watch(
activityListNotifierProvider(currentFilter).notifier,
);
final bodyView = _buildActivityList(context, ref, currentFilter);
return Column(
spacing: 8,
children: [
filterBar.padding(horizontal: 8, top: 8),
Expanded(
child: ExtendedRefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: CustomScrollView(
slivers: [
if (user.value != null)
SliverToBoxAdapter(
child: CheckInWidget(
margin: const EdgeInsets.only(bottom: 8),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: PostFeaturedList(),
),
),
if (notificationCount.value != null &&
notificationCount.value! > 0)
SliverToBoxAdapter(
child: notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: const EdgeInsets.only(bottom: 8),
),
),
bodyView,
SliverGap(getTabbedPadding(context).bottom),
],
),
).padding(horizontal: 8),
),
),
],
); );
} }
} }
@@ -464,7 +516,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
}; };
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -505,92 +557,60 @@ class _ActivityListView extends HookConsumerWidget {
final CursorPagingData<SnActivity> data; final CursorPagingData<SnActivity> data;
final int widgetCount; final int widgetCount;
final Widget endItemView; final Widget endItemView;
final bool contentOnly;
final ActivityListNotifier activitiesNotifier; final ActivityListNotifier activitiesNotifier;
final bool isWide;
const _ActivityListView({ const _ActivityListView({
required this.data, required this.data,
required this.widgetCount, required this.widgetCount,
required this.endItemView, required this.endItemView,
required this.activitiesNotifier, required this.activitiesNotifier,
this.contentOnly = false, required this.isWide,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider); return SliverList.separated(
itemCount: widgetCount,
separatorBuilder: (_, _) => const Gap(8),
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final notificationCount = ref.watch( final item = data.items[index];
notificationUnreadCountNotifierProvider, if (item.data == null) {
); return const SizedBox.shrink();
}
Widget itemWidget;
return CustomScrollView( switch (item.type) {
slivers: [ case 'posts.new':
SliverGap(12), case 'posts.new.replies':
if (user.value != null && !contentOnly) itemWidget = PostActionableItem(
SliverToBoxAdapter( borderRadius: 8,
child: CheckInWidget( item: SnPost.fromJson(item.data!),
margin: EdgeInsets.only(left: 8, right: 8, bottom: 4), onRefresh: () {
), activitiesNotifier.forceRefresh();
), },
if (!contentOnly) onUpdate: (post) {
SliverToBoxAdapter( activitiesNotifier.updateOne(
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4), index,
), item.copyWith(data: post.toJson()),
if (!contentOnly && (notificationCount.value ?? 0) > 0)
SliverToBoxAdapter(
child: notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
),
),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final item = data.items[index];
if (item.data == null) {
return const SizedBox.shrink();
}
Widget itemWidget;
switch (item.type) {
case 'posts.new':
case 'posts.new.replies':
itemWidget = PostActionableItem(
borderRadius: 8,
item: SnPost.fromJson(item.data!),
onRefresh: () {
activitiesNotifier.forceRefresh();
},
onUpdate: (post) {
activitiesNotifier.updateOne(
index,
item.copyWith(data: post.toJson()),
);
},
); );
itemWidget = Card( },
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), );
child: itemWidget, itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget);
); break;
break; case 'discovery':
case 'discovery': itemWidget = _DiscoveryActivityItem(data: item.data!);
itemWidget = _DiscoveryActivityItem(data: item.data!); break;
break; default:
default: itemWidget = const Placeholder();
itemWidget = const Placeholder(); }
}
return itemWidget; return itemWidget;
}, },
),
SliverGap(getTabbedPadding(context).bottom),
],
); );
} }
} }

View File

@@ -68,68 +68,84 @@ class TabsScreen extends HookConsumerWidget {
final currentIndex = getCurrentIndex(); final currentIndex = getCurrentIndex();
if (isWideScreen(context)) { if (isWideScreen(context)) {
return Row( return Container(
children: [ color: Theme.of(context).colorScheme.surfaceContainer,
NavigationRail( child: Row(
destinations: children: [
destinations NavigationRail(
.map( backgroundColor: Colors.transparent,
(e) => NavigationRailDestination( destinations:
icon: e.icon, destinations
label: Text(e.label), .map(
), (e) => NavigationRailDestination(
) icon: e.icon,
.toList(), label: Text(e.label),
selectedIndex: currentIndex, ),
onDestinationSelected: onDestinationSelected, )
), .toList(),
const VerticalDivider(width: 1), selectedIndex: currentIndex,
Expanded(child: child ?? const SizedBox.shrink()), onDestinationSelected: onDestinationSelected,
], ),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child ?? const SizedBox.shrink(),
),
),
],
),
); );
} }
return Stack( return Container(
children: [ color: Theme.of(context).colorScheme.surfaceContainer,
Positioned.fill(child: child ?? const SizedBox.shrink()), child: Stack(
Positioned( children: [
left: 0, Positioned.fill(
right: 0,
bottom: 0,
child: ConditionalBottomNav(
child: ClipRRect( child: ClipRRect(
child: BackdropFilter( borderRadius: const BorderRadius.all(Radius.circular(16)),
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: child ?? const SizedBox.shrink(),
child: Container( ),
decoration: BoxDecoration( ),
color: Theme.of( Positioned(
context, left: 0,
).colorScheme.surface.withOpacity(0.8), right: 0,
), bottom: 0,
child: MediaQuery.removePadding( child: ConditionalBottomNav(
context: context, child: ClipRRect(
removeTop: true, child: BackdropFilter(
child: NavigationBar( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
backgroundColor: Colors.transparent, child: Container(
shadowColor: Colors.transparent, decoration: BoxDecoration(
overlayColor: const WidgetStatePropertyAll( color: Theme.of(
Colors.transparent, context,
).colorScheme.surface.withOpacity(0.8),
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: NavigationBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
overlayColor: const WidgetStatePropertyAll(
Colors.transparent,
),
surfaceTintColor: Colors.transparent,
height: 56,
labelBehavior:
NavigationDestinationLabelBehavior.alwaysHide,
selectedIndex: currentIndex,
onDestinationSelected: onDestinationSelected,
destinations: destinations,
), ),
surfaceTintColor: Colors.transparent,
height: 56,
labelBehavior:
NavigationDestinationLabelBehavior.alwaysHide,
selectedIndex: currentIndex,
onDestinationSelected: onDestinationSelected,
destinations: destinations,
), ),
), ),
), ),
), ),
), ),
), ),
), ],
], ),
); );
} }
} }

View File

@@ -68,41 +68,33 @@ class WindowScaffold extends HookConsumerWidget {
if (!kIsWeb && if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Material( return Material(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Column( Column(
children: [ children: [
Container( DragToMoveArea(
decoration: BoxDecoration( child: Row(
border: Border( crossAxisAlignment: CrossAxisAlignment.center,
bottom: BorderSide( mainAxisAlignment:
color: Theme.of(context).dividerColor, Platform.isMacOS
width: 1 / devicePixelRatio, ? MainAxisAlignment.center
), : MainAxisAlignment.start,
), children: [
), Expanded(
child: DragToMoveArea( child:
child: Row( Platform.isMacOS
crossAxisAlignment: CrossAxisAlignment.center, ? Text(
mainAxisAlignment:
Platform.isMacOS
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
Expanded(
child: Platform.isMacOS
? Text(
'Solar Network', 'Solar Network',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).padding(horizontal: 12, vertical: 5) ).padding(horizontal: 12, vertical: 5)
: Row( : Row(
children: [ children: [
Image.asset( Image.asset(
Theme.of(context).brightness == Brightness.dark Theme.of(context).brightness ==
Brightness.dark
? 'assets/icons/icon-dark.png' ? 'assets/icons/icon-dark.png'
: 'assets/icons/icon.png', : 'assets/icons/icon.png',
width: 20, width: 20,
@@ -115,42 +107,45 @@ class WindowScaffold extends HookConsumerWidget {
), ),
], ],
).padding(horizontal: 12, vertical: 5), ).padding(horizontal: 12, vertical: 5),
), ),
if (!Platform.isMacOS) if (!Platform.isMacOS)
...([ ...([
IconButton( IconButton(
icon: Icon(Symbols.minimize), icon: Icon(Symbols.minimize),
onPressed: () => windowManager.minimize(), onPressed: () => windowManager.minimize(),
iconSize: 16, iconSize: 16,
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
constraints: BoxConstraints(), constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color, color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(
isMaximized.value
? Symbols.fullscreen_exit
: Symbols.fullscreen,
), ),
IconButton( onPressed: () async {
icon: Icon(isMaximized.value ? Symbols.fullscreen_exit : Symbols.fullscreen), if (await windowManager.isMaximized()) {
onPressed: () async { windowManager.restore();
if (await windowManager.isMaximized()) { } else {
windowManager.restore(); windowManager.maximize();
} else { }
windowManager.maximize(); },
} iconSize: 16,
}, padding: EdgeInsets.all(8),
iconSize: 16, constraints: BoxConstraints(),
padding: EdgeInsets.all(8), color: Theme.of(context).iconTheme.color,
constraints: BoxConstraints(), ),
color: Theme.of(context).iconTheme.color, IconButton(
), icon: Icon(Symbols.close),
IconButton( onPressed: () => windowManager.hide(),
icon: Icon(Symbols.close), iconSize: 16,
onPressed: () => windowManager.hide(), padding: EdgeInsets.all(8),
iconSize: 16, constraints: BoxConstraints(),
padding: EdgeInsets.all(8), color: Theme.of(context).iconTheme.color,
constraints: BoxConstraints(), ),
color: Theme.of(context).iconTheme.color, ]),
), ],
]),
],
),
), ),
), ),
Expanded(child: child), Expanded(child: child),