4da36aa31d
Flutter Android client for FabledAssistant with: - Session-cookie auth via persistent cookie jar (Dio + cookie_jar) - OAuth/SSO login via in-app WebView (flutter_inappwebview) - Notes: list, detail (markdown render), create/edit - Tasks: list with status tabs, create/edit with priority - Chat: SSE streaming bubbles, conversation management - Quick Capture FAB for rapid note/task creation - Settings screen (change server URL, logout) - Android home screen widget → opens chat - Riverpod state management, go_router navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
5.6 KiB
Dart
194 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'core/constants.dart';
|
|
import 'providers/auth_provider.dart';
|
|
import 'providers/settings_provider.dart';
|
|
import 'screens/auth/login_screen.dart';
|
|
import 'screens/chat/chat_screen.dart';
|
|
import 'screens/chat/conversations_list_screen.dart';
|
|
import 'screens/notes/note_detail_screen.dart';
|
|
import 'screens/notes/note_edit_screen.dart';
|
|
import 'screens/notes/notes_list_screen.dart';
|
|
import 'screens/quick_capture/quick_capture_screen.dart';
|
|
import 'screens/settings/settings_screen.dart';
|
|
import 'screens/setup/setup_screen.dart';
|
|
import 'screens/splash/splash_screen.dart';
|
|
import 'screens/tasks/task_edit_screen.dart';
|
|
import 'screens/tasks/tasks_list_screen.dart';
|
|
|
|
// ChangeNotifier that fires when auth or server URL changes,
|
|
// used as GoRouter.refreshListenable so the router re-evaluates redirects
|
|
// without being recreated.
|
|
class _RouterNotifier extends ChangeNotifier {
|
|
_RouterNotifier(Ref ref) {
|
|
ref.listen(authProvider, (_, _) => notifyListeners());
|
|
ref.listen(serverUrlProvider, (_, _) => notifyListeners());
|
|
}
|
|
}
|
|
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final notifier = _RouterNotifier(ref);
|
|
|
|
return GoRouter(
|
|
refreshListenable: notifier,
|
|
initialLocation: Routes.splash,
|
|
redirect: (context, state) {
|
|
final location = state.matchedLocation;
|
|
final serverUrl = ref.read(serverUrlProvider);
|
|
final authStatus = ref.read(authProvider);
|
|
|
|
if (serverUrl == null || serverUrl.isEmpty) {
|
|
if (location != Routes.setup) return Routes.setup;
|
|
return null;
|
|
}
|
|
|
|
if (authStatus == AuthStatus.unauthenticated) {
|
|
if (location != Routes.login && location != Routes.setup) {
|
|
return Routes.login;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(
|
|
path: Routes.splash,
|
|
builder: (_, _) => const SplashScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.setup,
|
|
builder: (_, _) => const SetupScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.login,
|
|
builder: (_, _) => const LoginScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.quickCapture,
|
|
builder: (_, _) => const QuickCaptureScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.settings,
|
|
builder: (_, _) => const SettingsScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.noteNew,
|
|
builder: (_, _) => const NoteEditScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.noteDetail,
|
|
builder: (_, state) => NoteDetailScreen(
|
|
noteId: int.parse(state.pathParameters['id']!),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: Routes.noteEdit,
|
|
builder: (_, state) => NoteEditScreen(
|
|
noteId: int.parse(state.pathParameters['id']!),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: Routes.taskNew,
|
|
builder: (_, _) => const TaskEditScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.taskEdit,
|
|
builder: (_, state) => TaskEditScreen(
|
|
taskId: int.parse(state.pathParameters['id']!),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: Routes.chat,
|
|
builder: (_, state) => ChatScreen(
|
|
conversationId: int.parse(state.pathParameters['id']!),
|
|
),
|
|
),
|
|
ShellRoute(
|
|
builder: (context, state, child) => _Shell(child: child),
|
|
routes: [
|
|
GoRoute(
|
|
path: Routes.notes,
|
|
builder: (_, _) => const NotesListScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.tasks,
|
|
builder: (_, _) => const TasksListScreen(),
|
|
),
|
|
GoRoute(
|
|
path: Routes.conversations,
|
|
builder: (_, _) => const ConversationsListScreen(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
class _Shell extends ConsumerWidget {
|
|
final Widget child;
|
|
const _Shell({required this.child});
|
|
|
|
static const _tabs = [Routes.notes, Routes.tasks, Routes.conversations];
|
|
|
|
int _tabIndex(String location) {
|
|
for (var i = 0; i < _tabs.length; i++) {
|
|
if (location.startsWith(_tabs[i])) return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final location = GoRouterState.of(context).matchedLocation;
|
|
final index = _tabIndex(location);
|
|
|
|
return Scaffold(
|
|
body: child,
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: index,
|
|
onDestinationSelected: (i) => context.go(_tabs[i]),
|
|
destinations: const [
|
|
NavigationDestination(icon: Icon(Icons.note), label: 'Notes'),
|
|
NavigationDestination(icon: Icon(Icons.check_box), label: 'Tasks'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.chat_bubble), label: 'Chat'),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
heroTag: 'shell_fab',
|
|
onPressed: () => context.push(Routes.quickCapture),
|
|
child: const Icon(Icons.add),
|
|
),
|
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
|
);
|
|
}
|
|
}
|
|
|
|
class FabledApp extends ConsumerWidget {
|
|
const FabledApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final router = ref.watch(routerProvider);
|
|
|
|
return MaterialApp.router(
|
|
title: 'Fabled',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
|
useMaterial3: true,
|
|
),
|
|
darkTheme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: Colors.indigo,
|
|
brightness: Brightness.dark,
|
|
),
|
|
useMaterial3: true,
|
|
),
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|