From 1fd34eb2a3f0450a7e3a3d590ade34a383f145a6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 12 Oct 2025 21:32:34 +0800 Subject: [PATCH] :lipstick: Merge the creator hub and developer hub to the tabs --- lib/route.dart | 362 +++++++++--------- lib/screens/account.dart | 182 +++++---- lib/screens/tabs.dart | 38 +- lib/widgets/activity_heatmap.dart | 15 +- lib/widgets/app_scaffold.dart | 72 ++-- .../navigation/conditional_bottom_nav.dart | 11 +- 6 files changed, 358 insertions(+), 322 deletions(-) diff --git a/lib/route.dart b/lib/route.dart index 272c38b9..55dc87ec 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -146,185 +146,6 @@ final routerProvider = Provider((ref) { return EventCalanderScreen(name: name); }, ), - GoRoute( - name: 'creatorHub', - path: '/creators', - builder: (context, state) => const CreatorHubScreen(), - routes: [ - // Web Feed Routes - GoRoute( - name: 'creatorFeeds', - path: ':name/feeds', - builder: (context, state) { - final name = state.pathParameters['name']!; - return WebFeedListScreen(pubName: name); - }, - ), - GoRoute( - name: 'creatorPosts', - path: ':name/posts', - builder: (context, state) { - final name = state.pathParameters['name']!; - return CreatorPostListScreen(pubName: name); - }, - ), - // Poll list route - GoRoute( - name: 'creatorPolls', - path: ':name/polls', - builder: (context, state) { - final name = state.pathParameters['name']!; - return CreatorPollListScreen(pubName: name); - }, - ), - // Poll routes - GoRoute( - name: 'creatorPollNew', - path: ':name/polls/new', - builder: (context, state) { - final name = state.pathParameters['name']!; - // initialPollId left null for create; initialPublisher prefilled - return PollEditorScreen(initialPublisher: name); - }, - ), - GoRoute( - name: 'creatorPollEdit', - path: ':name/polls/:id/edit', - builder: (context, state) { - final name = state.pathParameters['name']!; - final id = state.pathParameters['id']!; - return PollEditorScreen( - initialPollId: id, - initialPublisher: name, - ); - }, - ), - GoRoute( - name: 'creatorStickers', - path: ':name/stickers', - builder: (context, state) { - final name = state.pathParameters['name']!; - return StickersScreen(pubName: name); - }, - ), - GoRoute( - name: 'creatorNew', - path: 'new', - builder: (context, state) => const NewPublisherScreen(), - ), - GoRoute( - name: 'creatorEdit', - path: ':name/edit', - builder: (context, state) { - final name = state.pathParameters['name']!; - return EditPublisherScreen(name: name); - }, - ), - ], - ), - GoRoute( - name: 'developerHub', - path: '/developers', - builder: - (context, state) => DeveloperHubScreen( - initialPublisherName: state.uri.queryParameters['publisher'], - initialProjectId: state.uri.queryParameters['project'], - ), - routes: [ - GoRoute( - name: 'developerProjectNew', - path: ':name/projects/new', - builder: - (context, state) => NewProjectScreen( - publisherName: state.pathParameters['name']!, - ), - ), - GoRoute( - name: 'developerProjectEdit', - path: ':name/projects/:id/edit', - builder: - (context, state) => EditProjectScreen( - publisherName: state.pathParameters['name']!, - id: state.pathParameters['id']!, - ), - ), - GoRoute( - name: 'developerProjectDetail', - path: ':name/projects/:projectId', - builder: (context, state) { - final name = state.pathParameters['name']!; - final projectId = state.pathParameters['projectId']!; - // Redirect to hub with project selected - WidgetsBinding.instance.addPostFrameCallback((_) { - context.go( - '/developers?publisher=$name&project=$projectId', - ); - }); - return const SizedBox.shrink(); // Temporary placeholder - }, - routes: [ - GoRoute( - name: 'developerAppNew', - path: 'apps/new', - builder: - (context, state) => NewCustomAppScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - ), - ), - GoRoute( - name: 'developerAppEdit', - path: 'apps/:id/edit', - builder: - (context, state) => EditAppScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - id: state.pathParameters['id']!, - ), - ), - GoRoute( - name: 'developerAppDetail', - path: 'apps/:appId', - builder: - (context, state) => AppDetailScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - appId: state.pathParameters['appId']!, - ), - ), - GoRoute( - name: 'developerBotNew', - path: 'bots/new', - builder: - (context, state) => NewBotScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - ), - ), - GoRoute( - name: 'developerBotDetail', - path: 'bots/:botId', - builder: - (context, state) => BotDetailScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - botId: state.pathParameters['botId']!, - ), - ), - GoRoute( - name: 'developerBotEdit', - path: 'bots/:id/edit', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - id: state.pathParameters['id']!, - ), - ), - ], - ), - ], - ), // Web articles GoRoute( @@ -639,6 +460,189 @@ final routerProvider = Provider((ref) { return AccountProfileScreen(name: name); }, ), + // Creator hub tab + GoRoute( + name: 'creatorHub', + path: '/creators', + builder: (context, state) => const CreatorHubScreen(), + routes: [ + // Web Feed Routes + GoRoute( + name: 'creatorFeeds', + path: ':name/feeds', + builder: (context, state) { + final name = state.pathParameters['name']!; + return WebFeedListScreen(pubName: name); + }, + ), + GoRoute( + name: 'creatorPosts', + path: ':name/posts', + builder: (context, state) { + final name = state.pathParameters['name']!; + return CreatorPostListScreen(pubName: name); + }, + ), + // Poll list route + GoRoute( + name: 'creatorPolls', + path: ':name/polls', + builder: (context, state) { + final name = state.pathParameters['name']!; + return CreatorPollListScreen(pubName: name); + }, + ), + // Poll routes + GoRoute( + name: 'creatorPollNew', + path: ':name/polls/new', + builder: (context, state) { + final name = state.pathParameters['name']!; + // initialPollId left null for create; initialPublisher prefilled + return PollEditorScreen(initialPublisher: name); + }, + ), + GoRoute( + name: 'creatorPollEdit', + path: ':name/polls/:id/edit', + builder: (context, state) { + final name = state.pathParameters['name']!; + final id = state.pathParameters['id']!; + return PollEditorScreen( + initialPollId: id, + initialPublisher: name, + ); + }, + ), + GoRoute( + name: 'creatorStickers', + path: ':name/stickers', + builder: (context, state) { + final name = state.pathParameters['name']!; + return StickersScreen(pubName: name); + }, + ), + GoRoute( + name: 'creatorNew', + path: 'new', + builder: (context, state) => const NewPublisherScreen(), + ), + GoRoute( + name: 'creatorEdit', + path: ':name/edit', + builder: (context, state) { + final name = state.pathParameters['name']!; + return EditPublisherScreen(name: name); + }, + ), + ], + ), + + // Developer hub tab + GoRoute( + name: 'developerHub', + path: '/developers', + builder: + (context, state) => DeveloperHubScreen( + initialPublisherName: + state.uri.queryParameters['publisher'], + initialProjectId: state.uri.queryParameters['project'], + ), + routes: [ + GoRoute( + name: 'developerProjectNew', + path: ':name/projects/new', + builder: + (context, state) => NewProjectScreen( + publisherName: state.pathParameters['name']!, + ), + ), + GoRoute( + name: 'developerProjectEdit', + path: ':name/projects/:id/edit', + builder: + (context, state) => EditProjectScreen( + publisherName: state.pathParameters['name']!, + id: state.pathParameters['id']!, + ), + ), + GoRoute( + name: 'developerProjectDetail', + path: ':name/projects/:projectId', + builder: (context, state) { + final name = state.pathParameters['name']!; + final projectId = state.pathParameters['projectId']!; + // Redirect to hub with project selected + WidgetsBinding.instance.addPostFrameCallback((_) { + context.go( + '/developers?publisher=$name&project=$projectId', + ); + }); + return const SizedBox.shrink(); // Temporary placeholder + }, + routes: [ + GoRoute( + name: 'developerAppNew', + path: 'apps/new', + builder: + (context, state) => NewCustomAppScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + ), + ), + GoRoute( + name: 'developerAppEdit', + path: 'apps/:id/edit', + builder: + (context, state) => EditAppScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + id: state.pathParameters['id']!, + ), + ), + GoRoute( + name: 'developerAppDetail', + path: 'apps/:appId', + builder: + (context, state) => AppDetailScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + appId: state.pathParameters['appId']!, + ), + ), + GoRoute( + name: 'developerBotNew', + path: 'bots/new', + builder: + (context, state) => NewBotScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + ), + ), + GoRoute( + name: 'developerBotDetail', + path: 'bots/:botId', + builder: + (context, state) => BotDetailScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + botId: state.pathParameters['botId']!, + ), + ), + GoRoute( + name: 'developerBotEdit', + path: 'bots/:id/edit', + builder: + (context, state) => EditBotScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + id: state.pathParameters['id']!, + ), + ), + ], + ), + ], + ), ], ), ], diff --git a/lib/screens/account.dart b/lib/screens/account.dart index cf4b3cfc..fd7543de 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -149,132 +149,124 @@ class AccountScreen extends HookConsumerWidget { context.pushNamed('leveling'); }, ).padding(horizontal: 12), + if (!isWideScreen(context)) const SizedBox.shrink(), + if (!isWideScreen(context)) + Row( + spacing: 8, + children: [ + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: InkWell( + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Symbols.draw, size: 28).padding(bottom: 8), + Text( + 'creatorHub', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).tr().fontSize(16).bold(), + Text( + 'creatorHubDescription', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).tr(), + ], + ).padding(horizontal: 16, vertical: 12), + onTap: () { + context.goNamed('creatorHub'); + }, + ), + ).height(140), + ), + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: InkWell( + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Symbols.code, size: 28).padding(bottom: 8), + Text( + 'developerPortal', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).tr().fontSize(16).bold(), + Text( + 'developerPortalDescription', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).tr(), + ], + ).padding(horizontal: 16, vertical: 12), + onTap: () { + context.pushNamed('developerHub'); + }, + ), + ).height(140), + ), + ], + ).padding(horizontal: 12), const SizedBox.shrink(), - Row( - spacing: 8, - children: [ - Expanded( - child: Card( + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + spacing: 8, + children: [ + Card( margin: EdgeInsets.zero, child: InkWell( borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + spacing: 8, children: [ - Icon(Symbols.draw, size: 28).padding(bottom: 8), - Text( - 'creatorHub', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).tr().fontSize(16).bold(), - Text( - 'creatorHubDescription', - maxLines: 2, - overflow: TextOverflow.ellipsis, - ).tr(), - ], - ).padding(horizontal: 16, vertical: 12), - onTap: () { - context.goNamed('creatorHub'); - }, - ), - ).height(140), - ), - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: InkWell( - borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Symbols.code, size: 28).padding(bottom: 8), - Text( - 'developerPortal', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).tr().fontSize(16).bold(), - Text( - 'developerPortalDescription', - maxLines: 2, - overflow: TextOverflow.ellipsis, - ).tr(), - ], - ).padding(horizontal: 16, vertical: 12), - onTap: () { - context.pushNamed('developerHub'); - }, - ), - ).height(140), - ), - ], - ).padding(horizontal: 12), - const SizedBox.shrink(), - Row( - spacing: 8, - children: [ - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: InkWell( - borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Symbols.settings, size: 28).padding(bottom: 8), - Text('appSettings').tr().fontSize(16).bold(), + Icon(Symbols.settings, size: 20), + Text('appSettings').tr().fontSize(13).bold(), ], ).padding(horizontal: 16, vertical: 12), onTap: () { context.pushNamed('settings'); }, ), - ).height(120), - ), - Expanded( - child: Card( + ), + Card( margin: EdgeInsets.zero, child: InkWell( borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + spacing: 8, children: [ - Icon( - Symbols.person_edit, - size: 28, - ).padding(bottom: 8), - Text('updateYourProfile').tr().fontSize(16).bold(), + Icon(Symbols.person_edit, size: 20), + Text('updateYourProfile').tr().fontSize(13).bold(), ], ).padding(horizontal: 16, vertical: 12), onTap: () { context.pushNamed('profileUpdate'); }, ), - ).height(120), - ), - Expanded( - child: Card( + ), + Card( margin: EdgeInsets.zero, child: InkWell( borderRadius: BorderRadius.circular(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + spacing: 8, children: [ - Icon( - Symbols.manage_accounts, - size: 28, - ).padding(bottom: 8), - Text('accountSettings').tr().fontSize(16).bold(), + Icon(Symbols.manage_accounts, size: 20), + Text('accountSettings').tr().fontSize(13).bold(), ], ).padding(horizontal: 16, vertical: 12), onTap: () { context.pushNamed('accountSettings'); }, ), - ).height(120), - ), - ], - ).padding(horizontal: 12), + ), + ], + ).padding(horizontal: 12), + ).height(48), ListTile( minTileHeight: 48, leading: const Icon(Symbols.notifications), diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart index f86b6608..8b9dd678 100644 --- a/lib/screens/tabs.dart +++ b/lib/screens/tabs.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; import 'dart:ui'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,16 @@ import 'package:material_symbols_icons/symbols.dart'; final currentRouteProvider = StateProvider((ref) => null); +const kWideScreenRouteStart = 4; +const kTabRoutes = [ + '/', + '/chat', + '/realms', + '/account', + '/creators', + '/developers', +]; + class TabsScreen extends HookConsumerWidget { final Widget? child; const TabsScreen({super.key, this.child}); @@ -32,6 +43,8 @@ class TabsScreen extends HookConsumerWidget { notificationUnreadCountNotifierProvider, ); + final wideScreen = isWideScreen(context); + final destinations = [ NavigationDestination( label: 'explore'.tr(), @@ -50,19 +63,30 @@ class TabsScreen extends HookConsumerWidget { child: const Icon(Symbols.account_circle), ), ), + if (wideScreen) + NavigationDestination( + label: 'creatorHub'.tr(), + icon: const Icon(Symbols.draw), + ), + if (wideScreen) + NavigationDestination( + label: 'developerHub'.tr(), + icon: const Icon(Symbols.code), + ), ]; - final routes = ['/', '/chat', '/realms', '/account']; - int getCurrentIndex() { - if (currentLocation.startsWith('/chat')) return 1; - if (currentLocation.startsWith('/realms')) return 2; - if (currentLocation.startsWith('/account')) return 3; - return 0; // Default to explore + if (currentLocation == '/') return 0; + final idx = kTabRoutes.indexWhere( + (p) => currentLocation.startsWith(p), + 1, + ); + final value = math.max(idx, 0); + return math.min(value, destinations.length - 1); } void onDestinationSelected(int index) { - context.go(routes[index]); + context.go(kTabRoutes[index]); } final currentIndex = getCurrentIndex(); diff --git a/lib/widgets/activity_heatmap.dart b/lib/widgets/activity_heatmap.dart index 3127b6d5..c7e03277 100644 --- a/lib/widgets/activity_heatmap.dart +++ b/lib/widgets/activity_heatmap.dart @@ -5,9 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/heatmap.dart'; +import '../services/responsive.dart'; /// A reusable heatmap widget for displaying activity data in GitHub-style layout. -/// Shows exactly 365 days of data ending at the current date. +/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date. class ActivityHeatmapWidget extends HookConsumerWidget { final SnHeatmap heatmap; @@ -17,11 +18,13 @@ class ActivityHeatmapWidget extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedItem = useState(null); - // Generate exactly 365 days ending at current date final now = DateTime.now(); - // Start from exactly 365 days ago - final startDate = now.subtract(const Duration(days: 365)); + final isWide = isWideScreen(context); + final days = isWide ? 365 : 90; + + // Start from exactly the selected days ago + final startDate = now.subtract(Duration(days: days)); // End at current date final endDate = now; @@ -32,7 +35,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget { // Find sunday of the week containing end date final endSunday = endDate.add(Duration(days: 7 - endDate.weekday)); - // Generate weeks to cover exactly 365 days + // Generate weeks to cover the selected date range final weeks = []; var current = startMonday; while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) { @@ -45,7 +48,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget { for (final week in weeks) { for (var i = 0; i < 7; i++) { final date = week.add(Duration(days: i)); - // Only include dates within our 365-day range + // Only include dates within our selected range if (date.isAfter(startDate.subtract(const Duration(days: 1))) && date.isBefore(endDate.add(const Duration(days: 1)))) { final item = heatmap.items.firstWhere( diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 608ab21e..bb738a1f 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -68,26 +69,28 @@ class WindowScaffold extends HookConsumerWidget { return null; }, []); - final pageButtonActions = [ - IconButton( - icon: Icon(Symbols.keyboard_arrow_left), - onPressed: - ref.watch(routerProvider).canPop() - ? () => ref.read(routerProvider).pop() - : null, - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), - IconButton( - icon: Icon(Symbols.home), - onPressed: () => ref.read(routerProvider).go('/'), - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), + final router = ref.watch(routerProvider); + + final pageActionsButton = [ + if (router.canPop()) + IconButton( + icon: Icon(Symbols.close), + onPressed: router.canPop() ? () => router.pop() : null, + iconSize: 16, + padding: EdgeInsets.all(8), + constraints: BoxConstraints(), + color: Theme.of(context).iconTheme.color, + ) + else + IconButton( + icon: Icon(Symbols.home), + onPressed: () => router.go('/'), + iconSize: 16, + padding: EdgeInsets.all(8), + constraints: BoxConstraints(), + color: Theme.of(context).iconTheme.color, + ), + const Gap(8), ]; if (!kIsWeb && @@ -111,13 +114,18 @@ class WindowScaffold extends HookConsumerWidget { ? Stack( alignment: Alignment.center, children: [ - Row( - children: [ - if (Platform.isMacOS) - const SizedBox(width: 80), - ...pageButtonActions, - ], - ), + if (isWideScreen(context)) + Row( + key: Key( + 'app-page-action-${router.state.pageKey.value}', + ), + children: [ + const Spacer(), + ...pageActionsButton, + ], + ) + else + SizedBox(height: 32), Text( 'Solar Network', textAlign: TextAlign.center, @@ -374,7 +382,7 @@ class PageBackButton extends StatelessWidget { final isDesktop = !kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows); - if (isDesktop) return const SizedBox.shrink(); + if (isDesktop && isWideScreen(context)) return const SizedBox.shrink(); return IconButton( onPressed: () { @@ -387,9 +395,11 @@ class PageBackButton extends StatelessWidget { }, icon: Icon( color: color, - (!kIsWeb && (Platform.isMacOS || Platform.isIOS)) - ? Symbols.arrow_back_ios_new - : Symbols.arrow_back, + context.canPop() + ? (!kIsWeb && (Platform.isMacOS || Platform.isIOS)) + ? Symbols.arrow_back_ios_new + : Symbols.arrow_back + : Symbols.home, shadows: shadows, ), ); diff --git a/lib/widgets/navigation/conditional_bottom_nav.dart b/lib/widgets/navigation/conditional_bottom_nav.dart index f129e1a2..0c8710a9 100644 --- a/lib/widgets/navigation/conditional_bottom_nav.dart +++ b/lib/widgets/navigation/conditional_bottom_nav.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/screens/tabs.dart'; +import 'package:island/services/responsive.dart'; class ConditionalBottomNav extends HookConsumerWidget { final Widget child; @@ -17,10 +19,11 @@ class ConditionalBottomNav extends HookConsumerWidget { return null; }, [currentLocation]); - // Use the same route logic as TabsScreen for consistency - const mainTabRoutes = ['/', '/chat', '/realms', '/account']; - - final shouldShowBottomNav = mainTabRoutes.contains(currentLocation); + final routes = kTabRoutes.sublist( + 0, + isWideScreen(context) ? null : kWideScreenRouteStart, + ); + final shouldShowBottomNav = routes.contains(currentLocation); return shouldShowBottomNav ? child : const SizedBox.shrink(); }