main
dart 310 lines 12.9 KB
Raw
1 import 'package:cue/cue.dart';
2 import 'package:flutter/material.dart';
3 import 'package:flutter_riverpod/flutter_riverpod.dart';
4 import 'package:go_router/go_router.dart';
5 import 'package:hugeicons/hugeicons.dart';
6 import 'package:shadcn_ui/shadcn_ui.dart';
7
8 import '../../core/routes/app_routes.dart';
9 import '../../core/components/app_sizes.dart';
10 import '../../core/models/enums.dart';
11 import '../../l10n/app_localizations.dart';
12 import '../chat/providers/chat_providers.dart';
13 import '../lm_studio_catalog/views/lm_studio_download_widgets.dart';
14 import '../servers/providers/server_providers.dart';
15 import 'components/active_server_indicator.dart';
16 import 'components/conversation_drawer_header.dart';
17 import 'components/drawer_nav_item.dart';
18 import 'components/github_repo_card.dart';
19 import 'components/sidebar_search_button.dart';
20 import '../tts/views/components/tts_player_bar.dart';
21
22 class SidebarWidget extends ConsumerWidget {
23 const SidebarWidget({super.key});
24
25 @override
26 Widget build(BuildContext context, WidgetRef ref) {
27 final l10n = AppLocalizations.of(context)!;
28 final theme = Theme.of(context);
29 final location = GoRouterState.of(context).uri.toString();
30
31 final isHistory = location.startsWith(AppRoutes.chatHistory);
32 final isSavedMessages = location.startsWith(AppRoutes.savedMessages);
33 final isServers = location.startsWith(AppRoutes.servers);
34 final isMcpTools = location.startsWith(AppRoutes.mcpTools);
35 final isPersonas = location.startsWith(AppRoutes.personas);
36 final isLocalModels = location.startsWith(AppRoutes.onDeviceModels);
37 final isTtsModels = location.startsWith(AppRoutes.ttsModels);
38 final isCloudSync = location.startsWith(AppRoutes.cloudSync);
39 final isSettings = location == AppRoutes.settings;
40 final isHome = location == AppRoutes.home || location == '/';
41 final hasActiveChat = ref.watch(hasActiveChatSessionProvider);
42 final isTemporary = ref.watch(chatProvider.select((s) => s.isTemporary));
43 final activeServer = ref.watch(activeServerProvider);
44 final isLmStudio =
45 activeServer != null && activeServer.type == ServerType.lmStudio;
46
47 return Container(
48 width: AppSizes.sidebarWidth,
49 decoration: BoxDecoration(
50 color: theme.scaffoldBackgroundColor,
51 border: Directionality.of(context) == TextDirection.rtl
52 ? Border(left: BorderSide(color: theme.colorScheme.outline))
53 : Border(right: BorderSide(color: theme.colorScheme.outline)),
54 ),
55 child: SafeArea(
56 child: Cue.onMount(
57 motion: .smooth(),
58 child: Column(
59 children: [
60 Actor(
61 acts: [.fadeIn(), .slideX(from: -0.05)],
62 child: const ConversationDrawerHeader(),
63 ),
64
65 // New Chat Button
66 Actor(
67 delay: 60.ms,
68 acts: [.fadeIn(), .slideX(from: -0.04)],
69 child: Padding(
70 padding: const EdgeInsets.symmetric(
71 horizontal: 16,
72 vertical: 8,
73 ),
74 child: ShadButton(
75 width: double.infinity,
76 leading: const HugeIcon(
77 icon: HugeIcons.strokeRoundedAdd01,
78 size: 20,
79 ),
80 onPressed: () {
81 ref.read(chatProvider.notifier).startNewConversation();
82 context.go(AppRoutes.home);
83 if (Scaffold.maybeOf(context)?.isDrawerOpen ?? false) {
84 Navigator.pop(context);
85 }
86 },
87 child: Text(l10n.nav_new_chat),
88 ),
89 ),
90 ),
91
92 if (hasActiveChat)
93 Actor(
94 delay: 120.ms,
95 acts: [.fadeIn(), .slideX(from: -0.04)],
96 child: Padding(
97 padding: EdgeInsets.fromLTRB(16, 0, 16, isHome ? 8 : 8),
98 child: isTemporary
99 ? ShadButton.outline(
100 width: double.infinity,
101 leading: const HugeIcon(
102 icon: HugeIcons.strokeRoundedArrowLeft01,
103 size: 18,
104 ),
105 onPressed: () {
106 if (!isHome) context.go(AppRoutes.home);
107 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
108 false) {
109 Navigator.pop(context);
110 }
111 },
112 child: Text(l10n.return_to_temp_chat),
113 )
114 : ShadButton.secondary(
115 width: double.infinity,
116 leading: const HugeIcon(
117 icon: HugeIcons.strokeRoundedArrowLeft01,
118 size: 18,
119 ),
120 onPressed: () {
121 if (!isHome) context.go(AppRoutes.home);
122 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
123 false) {
124 Navigator.pop(context);
125 }
126 },
127 child: Text(l10n.return_to_chat),
128 ),
129 ),
130 ),
131
132 Actor(
133 delay: 180.ms,
134 acts: [.fadeIn()],
135 child: const SidebarSearchButton(),
136 ),
137 const SizedBox(height: 8),
138 const Divider(height: 1, indent: 16, endIndent: 16),
139 const SizedBox(height: 12),
140
141 // Primary Navigation
142 Expanded(
143 child: Actor(
144 delay: 220.ms,
145 acts: [.fadeIn(), .slideY(from: 0.04)],
146 child: SingleChildScrollView(
147 child: Column(
148 children: [
149 DrawerNavItem(
150 iconData: HugeIcons.strokeRoundedChatting01,
151 label: l10n.nav_history,
152 isSelected: isHistory,
153 onTap: () {
154 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
155 false) {
156 Navigator.pop(context);
157 }
158 context.go(AppRoutes.chatHistory);
159 },
160 ),
161 DrawerNavItem(
162 iconData: HugeIcons.strokeRoundedBookmark02,
163 label: l10n.nav_saved_messages,
164 isSelected: isSavedMessages,
165 onTap: () {
166 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
167 false) {
168 Navigator.pop(context);
169 }
170 context.go(AppRoutes.savedMessages);
171 },
172 ),
173 DrawerNavItem(
174 iconData: HugeIcons.strokeRoundedServerStack01,
175 label: l10n.nav_servers,
176 isSelected: isServers,
177 onTap: () {
178 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
179 false) {
180 Navigator.pop(context);
181 }
182 context.go(AppRoutes.servers);
183 },
184 ),
185 DrawerNavItem(
186 iconData: HugeIcons.strokeRoundedMcpServer,
187 label: l10n.mcp_tools_title,
188 isSelected: isMcpTools,
189 onTap: () {
190 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
191 false) {
192 Navigator.pop(context);
193 }
194 context.go(AppRoutes.mcpTools);
195 },
196 ),
197 if (isLmStudio)
198 DrawerNavItem(
199 iconData: HugeIcons.strokeRoundedAiSearch,
200 label: l10n.lm_studio_model_search,
201 isSelected: false,
202 trailing: const LmDownloadIndicatorButton(
203 compact: true,
204 ),
205 onTap: () {
206 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
207 false) {
208 Navigator.pop(context);
209 }
210 context.push(
211 AppRoutes.lmStudioModelBrowser,
212 extra: activeServer,
213 );
214 },
215 ),
216 DrawerNavItem(
217 iconData: HugeIcons.strokeRoundedSmartPhone01,
218 label: l10n.nav_local_models,
219 isSelected: isLocalModels,
220 onTap: () {
221 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
222 false) {
223 Navigator.pop(context);
224 }
225 context.go(AppRoutes.onDeviceModels);
226 },
227 ),
228 DrawerNavItem(
229 iconData: HugeIcons.strokeRoundedVoice,
230 label: l10n.nav_tts,
231 isSelected: isTtsModels,
232 onTap: () {
233 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
234 false) {
235 Navigator.pop(context);
236 }
237 context.go(AppRoutes.ttsModels);
238 },
239 ),
240 DrawerNavItem(
241 iconData: HugeIcons.strokeRoundedCompass01,
242 label: l10n.nav_personas,
243 isSelected: isPersonas,
244 onTap: () {
245 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
246 false) {
247 Navigator.pop(context);
248 }
249 context.go(AppRoutes.personas);
250 },
251 ),
252 const SizedBox(height: 8),
253 const Divider(height: 1, indent: 16, endIndent: 16),
254 const SizedBox(height: 8),
255 DrawerNavItem(
256 iconData: HugeIcons.strokeRoundedCloudSavingDone02,
257 label: l10n.cloud_sync,
258 isSelected: isCloudSync,
259 onTap: () {
260 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
261 false) {
262 Navigator.pop(context);
263 }
264 context.go(AppRoutes.cloudSync);
265 },
266 ),
267 DrawerNavItem(
268 iconData: HugeIcons.strokeRoundedSettings01,
269 label: l10n.nav_settings,
270 isSelected: isSettings,
271 onTap: () {
272 if (Scaffold.maybeOf(context)?.isDrawerOpen ??
273 false) {
274 Navigator.pop(context);
275 }
276 context.go(AppRoutes.settings);
277 },
278 ),
279 ],
280 ),
281 ),
282 ),
283 ),
284
285 Actor(
286 delay: 320.ms,
287 acts: [.fadeIn(), .slideY(from: 0.06)],
288 child: const TtsPlayerBar(),
289 ),
290
291 // Bottom Section
292 Actor(
293 delay: 360.ms,
294 acts: [.fadeIn(), .slideY(from: 0.08)],
295 child: const GitHubRepoCard(),
296 ),
297 const Divider(height: 1),
298 Actor(
299 delay: 400.ms,
300 acts: [.fadeIn()],
301 child: const ActiveServerIndicator(),
302 ),
303 const SizedBox(height: 16),
304 ],
305 ),
306 ),
307 ),
308 );
309 }
310 }