This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/lib/app.dart
T
bvandeusen 4da36aa31d Initial commit: Fabled Android app
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>
2026-02-28 21:28:53 -05:00

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,
);
}
}