mirror of
https://github.com/we-promise/sure.git
synced 2026-04-18 03:24:09 +00:00
Small Flutter UI tweaks
This commit is contained in:
@@ -65,6 +65,12 @@ class SureApp extends StatelessWidget {
|
||||
title: 'Sure Finance',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Geist',
|
||||
fontFamilyFallback: const [
|
||||
'Inter',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
],
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF6366F1),
|
||||
brightness: Brightness.light,
|
||||
@@ -96,6 +102,12 @@ class SureApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
fontFamily: 'Geist',
|
||||
fontFamilyFallback: const [
|
||||
'Inter',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
],
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF6366F1),
|
||||
brightness: Brightness.dark,
|
||||
|
||||
@@ -110,12 +110,19 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('AI Assistant'),
|
||||
title: const Text('Chats'),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _handleRefresh,
|
||||
tooltip: 'Refresh',
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, right: 12),
|
||||
child: InkWell(
|
||||
onTap: _handleRefresh,
|
||||
child: const SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Icon(Icons.refresh),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -373,7 +373,6 @@ class DashboardScreenState extends State<DashboardScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Dashboard'),
|
||||
actions: [
|
||||
if (_showSyncSuccess)
|
||||
Padding(
|
||||
|
||||
13
mobile/lib/screens/intro_screen.dart
Normal file
13
mobile/lib/screens/intro_screen.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'intro_screen_stub.dart' if (dart.library.html) 'intro_screen_web.dart';
|
||||
|
||||
class IntroScreen extends StatelessWidget {
|
||||
const IntroScreen({super.key, this.onStartChat});
|
||||
|
||||
final VoidCallback? onStartChat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntroScreenPlatform(onStartChat: onStartChat);
|
||||
}
|
||||
}
|
||||
39
mobile/lib/screens/intro_screen_stub.dart
Normal file
39
mobile/lib/screens/intro_screen_stub.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntroScreenPlatform extends StatelessWidget {
|
||||
const IntroScreenPlatform({super.key, this.onStartChat});
|
||||
|
||||
final VoidCallback? onStartChat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Intro experience coming soon',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
"We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. "
|
||||
'For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
242
mobile/lib/screens/intro_screen_web.dart
Normal file
242
mobile/lib/screens/intro_screen_web.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'dart:html' as html;
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntroScreenPlatform extends StatefulWidget {
|
||||
const IntroScreenPlatform({super.key, this.onStartChat});
|
||||
|
||||
final VoidCallback? onStartChat;
|
||||
|
||||
@override
|
||||
State<IntroScreenPlatform> createState() => _IntroScreenPlatformState();
|
||||
}
|
||||
|
||||
class _IntroScreenPlatformState extends State<IntroScreenPlatform> {
|
||||
static int _nextViewId = 0;
|
||||
|
||||
late final String _viewType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final currentId = _nextViewId;
|
||||
_nextViewId += 1;
|
||||
_viewType = 'intro-screen-web-$currentId';
|
||||
|
||||
ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
|
||||
final frame = html.IFrameElement()
|
||||
..srcdoc = _introHtmlContent
|
||||
..style.width = '100%'
|
||||
..style.height = '100%'
|
||||
..style.border = '0';
|
||||
|
||||
return frame;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SizedBox.expand(
|
||||
child: HtmlElementView(viewType: _viewType),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const String _introHtmlContent = '''
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Geist, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: #111827;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.grow {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.pt-0 {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.pb-4 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.max-w-5xl {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.space-y-2 > * + * {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.bg-container {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.intro-card-shell {
|
||||
width: min(95%, 48rem);
|
||||
box-sizing: border-box;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.shadow-border-xs {
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="grow overflow-y-auto px-3 lg:px-10 pt-0 pb-4 w-full mx-auto max-w-5xl" data-app-layout-target="content">
|
||||
<div class="mx-auto max-w-3xl intro-card-shell">
|
||||
<div class="bg-container shadow-border-xs rounded-2xl p-8 text-center space-y-4">
|
||||
<h2 class="text-xl font-semibold text-primary">Intro experience coming soon</h2>
|
||||
<p class="text-secondary">
|
||||
We're building a richer onboarding journey to learn about your goals, milestones, and day-to-day needs. For now, head over to the chat sidebar to start a conversation with Sure and let us know where you are in your financial journey.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
''';
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'chat_list_screen.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'intro_screen.dart';
|
||||
import 'more_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
@@ -17,13 +20,17 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
|
||||
int _currentIndex = 0;
|
||||
final _dashboardKey = GlobalKey<DashboardScreenState>();
|
||||
|
||||
List<Widget> _buildScreens(bool introLayout) {
|
||||
List<Widget> _buildScreens(bool introLayout, VoidCallback? onStartChat) {
|
||||
final screens = <Widget>[];
|
||||
|
||||
if (!introLayout) {
|
||||
screens.add(DashboardScreen(key: _dashboardKey));
|
||||
}
|
||||
|
||||
if (introLayout) {
|
||||
screens.add(IntroScreen(onStartChat: onStartChat));
|
||||
}
|
||||
|
||||
screens.add(const ChatListScreen());
|
||||
|
||||
if (!introLayout) {
|
||||
@@ -35,6 +42,36 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
|
||||
return screens;
|
||||
}
|
||||
|
||||
Future<void> _handleDestinationSelected(
|
||||
int index,
|
||||
AuthProvider authProvider,
|
||||
bool introLayout,
|
||||
) async {
|
||||
const chatIndex = 1;
|
||||
|
||||
if (index == chatIndex && !authProvider.aiEnabled) {
|
||||
final enabled = await _showEnableAiPrompt();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
if (!introLayout && index == 0) {
|
||||
_dashboardKey.currentState?.reloadPreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSelectSettings(AuthProvider authProvider, bool introLayout) async {
|
||||
final settingsIndex = introLayout ? 2 : 3;
|
||||
await _handleDestinationSelected(settingsIndex, authProvider, introLayout);
|
||||
}
|
||||
|
||||
List<NavigationDestination> _buildDestinations(bool introLayout) {
|
||||
final destinations = <NavigationDestination>[];
|
||||
|
||||
@@ -48,6 +85,16 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
if (introLayout) {
|
||||
destinations.add(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.auto_awesome_outlined),
|
||||
selectedIcon: Icon(Icons.auto_awesome),
|
||||
label: 'Intro',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
destinations.add(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.chat_bubble_outline),
|
||||
@@ -66,17 +113,48 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
destinations.add(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
);
|
||||
|
||||
return destinations;
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildTopBar(AuthProvider authProvider, bool introLayout) {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: 60,
|
||||
elevation: 0,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
actionsPadding: EdgeInsets.zero,
|
||||
title: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12, left: 12),
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/logomark.svg',
|
||||
width: 36,
|
||||
height: 36,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, right: 12),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_handleSelectSettings(authProvider, introLayout);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: Icon(Icons.settings_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _showEnableAiPrompt() async {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
@@ -116,43 +194,49 @@ class _MainNavigationScreenState extends State<MainNavigationScreen> {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
int _resolveBottomSelectedIndex(List<NavigationDestination> destinations) {
|
||||
if (destinations.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_currentIndex < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_currentIndex >= destinations.length) {
|
||||
return destinations.length - 1;
|
||||
}
|
||||
|
||||
return _currentIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AuthProvider>(
|
||||
builder: (context, authProvider, _) {
|
||||
final introLayout = authProvider.isIntroLayout;
|
||||
final screens = _buildScreens(introLayout);
|
||||
const chatIndex = 1;
|
||||
final screens = _buildScreens(
|
||||
introLayout,
|
||||
() => _handleDestinationSelected(chatIndex, authProvider, introLayout),
|
||||
);
|
||||
final destinations = _buildDestinations(introLayout);
|
||||
final bottomNavIndex = _resolveBottomSelectedIndex(destinations);
|
||||
|
||||
if (_currentIndex >= screens.length) {
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
final chatIndex = introLayout ? 0 : 1;
|
||||
final homeIndex = 0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: _buildTopBar(authProvider, introLayout),
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: screens,
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: (index) async {
|
||||
if (index == chatIndex && !authProvider.aiEnabled) {
|
||||
final enabled = await _showEnableAiPrompt();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
if (!introLayout && index == homeIndex) {
|
||||
_dashboardKey.currentState?.reloadPreferences();
|
||||
}
|
||||
selectedIndex: bottomNavIndex,
|
||||
onDestinationSelected: (index) {
|
||||
_handleDestinationSelected(index, authProvider, introLayout);
|
||||
},
|
||||
destinations: destinations,
|
||||
),
|
||||
|
||||
@@ -10,9 +10,6 @@ class MoreScreen extends StatelessWidget {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('More'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
_buildMenuItem(
|
||||
|
||||
Reference in New Issue
Block a user