diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 395bef1..984ed36 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -24,10 +24,14 @@
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
-
+ /> -->
+
+
@@ -44,6 +48,8 @@
+
+
diff --git a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt
index e61508f..0dd7a7f 100644
--- a/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt
+++ b/android/app/src/main/kotlin/org/aossie/ell_ena/MainActivity.kt
@@ -1,5 +1,86 @@
package org.aossie.ell_ena
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
-class MainActivity : FlutterActivity()
\ No newline at end of file
+class MainActivity : FlutterActivity() {
+ private val CHANNEL = "app_shortcuts"
+ private var pendingRoute: String? = null
+ private var methodChannel: MethodChannel? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ handleIntent(intent)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ handleIntent(intent)
+ setIntent(intent) // Important: update the current intent
+
+ // ✅ Send route to Flutter immediately when app is resumed
+ sendRouteToFlutter()
+ }
+
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
+
+ // Setup method call handler
+ methodChannel?.setMethodCallHandler { call, result ->
+ when (call.method) {
+ "getInitialRoute" -> {
+ result.success(pendingRoute)
+ pendingRoute = null // Clear after sending
+ }
+ else -> result.notImplemented()
+ }
+ }
+
+ // Try to send initial route immediately if Flutter is ready
+ sendRouteToFlutter()
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ // Method 1: Try to get route from deep link (app://shortcut/chat)
+ val uri: Uri? = intent?.data
+
+ if (uri != null && uri.scheme == "app" && uri.host == "shortcut") {
+ val route = uri.path?.removePrefix("/") // Remove leading "/"
+ pendingRoute = route
+ return
+ }
+
+ // Method 2: Try to get screen index from extras (fallback)
+ val screenIndex = intent?.getIntExtra("screen", -1)
+ if (screenIndex != -1) {
+ pendingRoute = when (screenIndex) {
+ 0 -> "dashboard"
+ 1 -> "calendar"
+ 2 -> "workspace"
+ 3 -> "chat"
+ 4 -> "profile"
+ else -> null
+ }
+ }
+ }
+
+ private fun sendRouteToFlutter() {
+ if (pendingRoute != null && methodChannel != null) {
+ try {
+ methodChannel?.invokeMethod("navigate", pendingRoute)
+ pendingRoute = null
+ } catch (e: Exception) {
+ // ✅ Added logging for observability
+ Log.w("MainActivity", "Failed to send pendingRoute to Flutter: ${e.message}")
+ // Flutter might not be ready yet, route will be sent when getInitialRoute is called
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/res/drawable-mdpi/ic_calendar.png b/android/app/src/main/res/drawable-mdpi/ic_calendar.png
new file mode 100644
index 0000000..a88f93d
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_calendar.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_chat.png b/android/app/src/main/res/drawable-mdpi/ic_chat.png
new file mode 100644
index 0000000..7d33ed8
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_chat.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_dashboard.png b/android/app/src/main/res/drawable-mdpi/ic_dashboard.png
new file mode 100644
index 0000000..58c9062
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_dashboard.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_profile.png b/android/app/src/main/res/drawable-mdpi/ic_profile.png
new file mode 100644
index 0000000..4f4d57f
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_profile.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_workspace.png b/android/app/src/main/res/drawable-mdpi/ic_workspace.png
new file mode 100644
index 0000000..eaf5cef
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_workspace.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_dashboard.png b/android/app/src/main/res/mipmap-hdpi/ic_dashboard.png
new file mode 100644
index 0000000..58c9062
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_dashboard.png differ
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0531b63
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+ Ell-ena
+
+
+ Dashboard
+ Dashboard
+
+ Calendar
+ Calendar
+
+ Workspace
+ Workspace
+
+ Chat
+ Chat
+
+ Profile
+ Profile
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 5fac679..a9f75e5 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,18 +1,14 @@
-
+
+
-
+
+
diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 0000000..d07f1c2
--- /dev/null
+++ b/android/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/main.dart b/lib/main.dart
index b5f4e77..70aefac 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,24 +1,28 @@
-
-
import 'package:flutter/material.dart';
+
import 'screens/splash_screen.dart';
import 'screens/home/home_screen.dart';
import 'screens/chat/chat_screen.dart';
+
import 'services/navigation_service.dart';
import 'services/supabase_service.dart';
import 'services/ai_service.dart';
+import 'services/app_shortcuts_service.dart'; // Add this import
void main() async {
WidgetsFlutterBinding.ensureInitialized();
-
+
try {
- await SupabaseService().initialize();
+ // Initialize app shortcuts service first
+ await AppShortcutsService.initialize();
+ // Initialize other services
+ await SupabaseService().initialize();
await AIService().initialize();
} catch (e) {
debugPrint('Error initializing services: $e');
}
-
+
runApp(const MyApp());
}
@@ -30,8 +34,12 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Ell-ena',
debugShowCheckedModeBanner: false,
+
navigatorKey: NavigationService().navigatorKey,
- navigatorObservers: [AppRouteObserver.instance],
+ navigatorObservers: [
+ AppRouteObserver.instance,
+ ],
+
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
@@ -49,37 +57,83 @@ class MyApp extends StatelessWidget {
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
- bodyLarge: TextStyle(fontSize: 16, letterSpacing: 0.5),
+ bodyLarge: TextStyle(
+ fontSize: 16,
+ letterSpacing: 0.5,
+ ),
),
),
+
home: const SplashScreen(),
+
onGenerateRoute: (settings) {
- if (settings.name == '/') {
- return MaterialPageRoute(
- builder: (context) => const SplashScreen(),
- settings: settings,
- );
- } else if (settings.name == '/home') {
- final args = settings.arguments as Map?;
- return MaterialPageRoute(
- builder: (context) => HomeScreen(arguments: args),
- settings: settings,
- );
- } else if (settings.name == '/chat') {
- final args = settings.arguments as Map?;
- return MaterialPageRoute(
- builder: (context) => ChatScreen(arguments: args),
- settings: settings,
- );
+ // Get initial shortcut if any
+ final initialShortcut = AppShortcutsService.getPendingShortcut();
+
+ // Convert shortcut to screen index
+ int getScreenIndex(String? shortcut) {
+ switch (shortcut) {
+ case 'dashboard': return 0;
+ case 'calendar': return 1;
+ case 'workspace': return 2;
+ case 'chat': return 3;
+ case 'profile': return 4;
+ default: return 0;
+ }
+ }
+
+ switch (settings.name) {
+ case '/':
+ return MaterialPageRoute(
+ builder: (_) => const SplashScreen(),
+ settings: settings,
+ );
+
+ case '/home':
+ Map? args;
+
+ // If there's an initial shortcut, use it
+ if (initialShortcut != null) {
+ args = {
+ 'screen': getScreenIndex(initialShortcut),
+ 'initial_route': initialShortcut
+ };
+ }
+
+ // Merge with any existing arguments
+ if (settings.arguments != null) {
+ args = {
+ ...args ?? {},
+ ...settings.arguments as Map,
+ };
+ }
+
+ return MaterialPageRoute(
+ builder: (_) => HomeScreen(arguments: args),
+ settings: settings,
+ );
+
+ case '/chat':
+ final args = settings.arguments as Map?;
+ return MaterialPageRoute(
+ builder: (_) => ChatScreen(arguments: args),
+ settings: settings,
+ );
+
+ default:
+ return MaterialPageRoute(
+ builder: (_) => const SplashScreen(),
+ settings: settings,
+ );
}
- return null;
},
);
}
}
-// Simple singleton RouteObserver to allow screens to refresh on focus
+/// Simple singleton RouteObserver
+/// Used so screens can refresh when they regain focus
class AppRouteObserver extends RouteObserver> {
AppRouteObserver._();
static final AppRouteObserver instance = AppRouteObserver._();
-}
+}
\ No newline at end of file
diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart
index c8cf293..1ab0a49 100644
--- a/lib/screens/auth/login_screen.dart
+++ b/lib/screens/auth/login_screen.dart
@@ -3,13 +3,16 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../widgets/custom_widgets.dart';
import '../../services/navigation_service.dart';
import '../../services/supabase_service.dart';
+import '../../services/app_shortcuts_service.dart';
import '../home/home_screen.dart';
import 'signup_screen.dart';
import 'forgot_password_screen.dart';
import 'team_selection_dialog.dart';
class LoginScreen extends StatefulWidget {
- const LoginScreen({super.key});
+ final Map? arguments;
+
+ const LoginScreen({super.key, this.arguments});
@override
State createState() => _LoginScreenState();
@@ -29,6 +32,10 @@ class _LoginScreenState extends State
@override
void initState() {
super.initState();
+
+ // Debug arguments
+ print('[LoginScreen] initState called with arguments: ${widget.arguments}');
+
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
@@ -68,15 +75,32 @@ class _LoginScreenState extends State
setState(() => _isLoading = true);
try {
- // Login with Supabase
final response = await _supabaseService.client.auth.signInWithPassword(
email: _emailController.text,
password: _passwordController.text,
);
if (response.user != null) {
+ // Merge pending shortcut and existing arguments
+ Map homeScreenArgs = Map.from(widget.arguments ?? {});
+ final pendingShortcut = AppShortcutsService.getPendingShortcut();
+
+ if (pendingShortcut != null) {
+ final screenIndex = _getScreenIndex(pendingShortcut);
+ if (screenIndex != null) {
+ homeScreenArgs['screen'] = screenIndex;
+ homeScreenArgs['initial_route'] = pendingShortcut;
+ }
+ }
+
+ print('[LoginScreen] Navigating to HomeScreen with args: $homeScreenArgs');
+
if (mounted) {
- NavigationService().navigateToReplacement(const HomeScreen());
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => HomeScreen(arguments: homeScreenArgs),
+ ),
+ );
}
} else {
if (mounted) {
@@ -89,15 +113,34 @@ class _LoginScreenState extends State
}
}
} catch (e) {
+ print('[LoginScreen] Login error: $e'); // Log for debugging
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
+ const SnackBar(
+ content: Text('Login failed. Please check your credentials and try again.'),
+ backgroundColor: Colors.red,
+ ),
);
}
} finally {
- if (mounted) {
- setState(() => _isLoading = false);
- }
+ if (mounted) setState(() => _isLoading = false);
+ }
+ }
+
+ int? _getScreenIndex(String? route) {
+ switch (route) {
+ case 'dashboard':
+ return 0;
+ case 'calendar':
+ return 1;
+ case 'workspace':
+ return 2;
+ case 'chat':
+ return 3;
+ case 'profile':
+ return 4;
+ default:
+ return null;
}
}
@@ -167,12 +210,8 @@ class _LoginScreenState extends State
label: 'Email',
icon: Icons.email_outlined,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your email';
- }
- if (!value.contains('@')) {
- return 'Please enter a valid email';
- }
+ if (value == null || value.isEmpty) return 'Please enter your email';
+ if (!value.contains('@')) return 'Please enter a valid email';
return null;
},
),
@@ -183,12 +222,8 @@ class _LoginScreenState extends State
icon: Icons.lock_outline,
isPassword: true,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your password';
- }
- if (value.length < 6) {
- return 'Password must be at least 6 characters';
- }
+ if (value == null || value.isEmpty) return 'Please enter your password';
+ if (value.length < 6) return 'Password must be at least 6 characters';
return null;
},
),
@@ -196,11 +231,7 @@ class _LoginScreenState extends State
Align(
alignment: Alignment.centerRight,
child: TextButton(
- onPressed: () {
- NavigationService().navigateTo(
- const ForgotPasswordScreen(),
- );
- },
+ onPressed: () => NavigationService().navigateTo(const ForgotPasswordScreen()),
child: Text(
'Forgot Password?',
style: TextStyle(
@@ -265,14 +296,11 @@ class _LoginScreenState extends State
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Text(
- 'Don\'t have an account? ',
- style: TextStyle(color: Colors.grey.shade400),
- ),
+ Text('Don\'t have an account? ', style: TextStyle(color: Colors.grey.shade400)),
TextButton(
- onPressed: () {
- NavigationService().navigateTo(const SignupScreen());
- },
+ onPressed: () => NavigationService().navigateTo(
+ SignupScreen(arguments: widget.arguments),
+ ),
child: Text(
'Sign Up',
style: TextStyle(
@@ -291,4 +319,4 @@ class _LoginScreenState extends State
],
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart
index 1074b6d..dcb5d9d 100644
--- a/lib/screens/auth/signup_screen.dart
+++ b/lib/screens/auth/signup_screen.dart
@@ -1,15 +1,17 @@
import 'package:flutter/material.dart';
-import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../widgets/custom_widgets.dart';
import '../../services/navigation_service.dart';
import '../../services/supabase_service.dart';
+import '../../services/app_shortcuts_service.dart';
import '../home/home_screen.dart';
import 'login_screen.dart';
import 'verify_otp_screen.dart';
import 'team_selection_dialog.dart';
class SignupScreen extends StatefulWidget {
- const SignupScreen({super.key});
+ final Map? arguments;
+
+ const SignupScreen({super.key, this.arguments});
@override
State createState() => _SignupScreenState();
@@ -32,10 +34,11 @@ class _SignupScreenState extends State
@override
void initState() {
super.initState();
+
+ print('[SignupScreen] initState called with arguments: ${widget.arguments}');
+
_tabController = TabController(length: 2, vsync: this);
- _tabController.addListener(() {
- setState(() {});
- });
+ _tabController.addListener(() => setState(() {}));
}
@override
@@ -50,17 +53,12 @@ class _SignupScreenState extends State
super.dispose();
}
- // Handle team creation
- Future _handleCreateTeam() async {
- if (!_createTeamFormKey.currentState!.validate()) return;
-
+ // Unified method to send verification OTP and navigate
+ Future _sendOtpAndNavigate(String verifyType, Map userData) async {
setState(() => _isLoading = true);
try {
- // Only send signup email without creating user upfront
- await _supabaseService.client.auth.signInWithOtp(
- email: _emailController.text,
- );
+ await _supabaseService.client.auth.signInWithOtp(email: _emailController.text);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -70,41 +68,54 @@ class _SignupScreenState extends State
),
);
+ // Merge existing arguments into userData
+ if (widget.arguments != null) userData['initial_args'] = widget.arguments;
+
+ print('[SignupScreen] Navigate to VerifyOTPScreen with userData: $userData');
+
NavigationService().navigateTo(
VerifyOTPScreen(
email: _emailController.text,
- verifyType: 'signup_create',
- userData: {
- 'teamName': _teamNameController.text,
- 'adminName': _nameController.text,
- 'password': _passwordController.text,
- },
+ verifyType: verifyType,
+ userData: userData,
),
);
}
} catch (e) {
+ print('[SignupScreen] Error sending OTP: $e'); // Log for debugging
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
+ const SnackBar(
+ content: Text('Failed to send verification email. Please try again.'),
+ backgroundColor: Colors.red,
+ ),
);
}
} finally {
- if (mounted) {
- setState(() => _isLoading = false);
- }
+ if (mounted) setState(() => _isLoading = false);
}
}
- // Handle joining a team
+ Future _handleCreateTeam() async {
+ if (!_createTeamFormKey.currentState!.validate()) return;
+
+ final userData = {
+ 'teamName': _teamNameController.text,
+ 'adminName': _nameController.text,
+ 'password': _passwordController.text,
+ };
+
+ await _sendOtpAndNavigate('signup_create', userData);
+ }
+
Future _handleJoinTeam() async {
if (!_joinTeamFormKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
- // First check if the team exists
- final teamExists =
- await _supabaseService.teamExists(_teamIdController.text);
+ // Check if the team exists
+ final teamExists = await _supabaseService.teamExists(_teamIdController.text);
if (!teamExists) {
if (mounted) {
@@ -114,93 +125,30 @@ class _SignupScreenState extends State
backgroundColor: Colors.red,
),
);
- setState(() => _isLoading = false);
}
- return;
- }
-
- // Only send signup email without creating user upfront
- await _supabaseService.client.auth.signInWithOtp(
- email: _emailController.text,
- );
-
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Verification email sent. Please check your inbox.'),
- backgroundColor: Colors.green,
- ),
- );
-
- NavigationService().navigateTo(
- VerifyOTPScreen(
- email: _emailController.text,
- verifyType: 'signup_join',
- userData: {
- 'teamId': _teamIdController.text,
- 'fullName': _nameController.text,
- 'password': _passwordController.text,
- },
- ),
- );
- }
- } catch (e) {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
- );
- }
- } finally {
- if (mounted) {
setState(() => _isLoading = false);
+ return;
}
- }
- }
- Future _handleGoogleSignIn() async {
- setState(() => _isLoading = true);
-
- try {
- final result = await _supabaseService.signInWithGoogle();
+ final userData = {
+ 'teamId': _teamIdController.text,
+ 'fullName': _nameController.text,
+ 'password': _passwordController.text,
+ };
- if (mounted) {
- if (result['success'] == true) {
- if (result['isNewUser'] == true) {
- // New user - show team selection dialog
- showDialog(
- context: context,
- barrierDismissible: false,
- builder: (context) => TeamSelectionDialog(
- userEmail: result['email'] ?? '',
- googleRefreshToken: result['googleRefreshToken'],
- ),
- );
- } else {
- // Existing user - go to home
- NavigationService().navigateToReplacement(const HomeScreen());
- }
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(result['error'] ?? 'Google sign-in failed'),
- backgroundColor: Colors.red,
- ),
- );
- }
- }
+ await _sendOtpAndNavigate('signup_join', userData);
} catch (e) {
+ print('[SignupScreen] Error checking team: $e'); // Log for debugging
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Error: $e'),
+ const SnackBar(
+ content: Text('Failed to verify team. Please try again.'),
backgroundColor: Colors.red,
),
);
}
} finally {
- if (mounted) {
- setState(() => _isLoading = false);
- }
+ if (mounted) setState(() => _isLoading = false);
}
}
@@ -223,11 +171,11 @@ class _SignupScreenState extends State
),
const SizedBox(height: 24),
SizedBox(
- height: 350, // Adjust height as needed
+ height: 350,
child: TabBarView(
controller: _tabController,
children: [
- // Join Team Tab
+ // Join Team Form
Form(
key: _joinTeamFormKey,
child: Column(
@@ -237,12 +185,8 @@ class _SignupScreenState extends State
label: 'Team ID',
icon: Icons.people_outline,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter team ID';
- }
- if (value.length != 6) {
- return 'Team ID must be 6 characters';
- }
+ if (value == null || value.isEmpty) return 'Please enter team ID';
+ if (value.length != 6) return 'Team ID must be 6 characters';
return null;
},
),
@@ -252,9 +196,7 @@ class _SignupScreenState extends State
label: 'Full Name',
icon: Icons.person_outline,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your name';
- }
+ if (value == null || value.isEmpty) return 'Please enter your name';
return null;
},
),
@@ -264,12 +206,8 @@ class _SignupScreenState extends State
label: 'Email',
icon: Icons.email_outlined,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your email';
- }
- if (!value.contains('@')) {
- return 'Please enter a valid email';
- }
+ if (value == null || value.isEmpty) return 'Please enter your email';
+ if (!value.contains('@')) return 'Please enter a valid email';
return null;
},
),
@@ -280,12 +218,8 @@ class _SignupScreenState extends State
icon: Icons.lock_outline,
isPassword: true,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your password';
- }
- if (value.length < 6) {
- return 'Password must be at least 6 characters';
- }
+ if (value == null || value.isEmpty) return 'Please enter your password';
+ if (value.length < 6) return 'Password must be at least 6 characters';
return null;
},
),
@@ -296,19 +230,15 @@ class _SignupScreenState extends State
icon: Icons.lock_outline,
isPassword: true,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please confirm your password';
- }
- if (value != _passwordController.text) {
- return 'Passwords do not match';
- }
+ if (value == null || value.isEmpty) return 'Please confirm your password';
+ if (value != _passwordController.text) return 'Passwords do not match';
return null;
},
),
],
),
),
- // Create Team Tab
+ // Create Team Form
Form(
key: _createTeamFormKey,
child: Column(
@@ -318,9 +248,7 @@ class _SignupScreenState extends State
label: 'Team Name',
icon: Icons.group,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter team name';
- }
+ if (value == null || value.isEmpty) return 'Please enter team name';
return null;
},
),
@@ -330,9 +258,7 @@ class _SignupScreenState extends State
label: 'Admin Name',
icon: Icons.person_outline,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter admin name';
- }
+ if (value == null || value.isEmpty) return 'Please enter admin name';
return null;
},
),
@@ -342,12 +268,8 @@ class _SignupScreenState extends State
label: 'Admin Email',
icon: Icons.email_outlined,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter admin email';
- }
- if (!value.contains('@')) {
- return 'Please enter a valid email';
- }
+ if (value == null || value.isEmpty) return 'Please enter admin email';
+ if (!value.contains('@')) return 'Please enter a valid email';
return null;
},
),
@@ -358,12 +280,8 @@ class _SignupScreenState extends State
icon: Icons.lock_outline,
isPassword: true,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter your password';
- }
- if (value.length < 6) {
- return 'Password must be at least 6 characters';
- }
+ if (value == null || value.isEmpty) return 'Please enter your password';
+ if (value.length < 6) return 'Password must be at least 6 characters';
return null;
},
),
@@ -374,12 +292,8 @@ class _SignupScreenState extends State
icon: Icons.lock_outline,
isPassword: true,
validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please confirm your password';
- }
- if (value != _passwordController.text) {
- return 'Passwords do not match';
- }
+ if (value == null || value.isEmpty) return 'Please confirm your password';
+ if (value != _passwordController.text) return 'Passwords do not match';
return null;
},
),
@@ -394,9 +308,7 @@ class _SignupScreenState extends State
text: _tabController.index == 0 ? 'Join Team' : 'Create Team',
onPressed: _isLoading
? null
- : (_tabController.index == 0
- ? _handleJoinTeam
- : _handleCreateTeam),
+ : (_tabController.index == 0 ? _handleJoinTeam : _handleCreateTeam),
isLoading: _isLoading,
),
const SizedBox(height: 24),
@@ -451,7 +363,7 @@ class _SignupScreenState extends State
text: 'Already have an account? Sign In',
onPressed: () {
NavigationService().navigateToReplacement(
- const LoginScreen(),
+ LoginScreen(arguments: widget.arguments),
);
},
isOutlined: true,
diff --git a/lib/screens/auth/verify_otp_screen.dart b/lib/screens/auth/verify_otp_screen.dart
index e5fc5ac..ca2c1c2 100644
--- a/lib/screens/auth/verify_otp_screen.dart
+++ b/lib/screens/auth/verify_otp_screen.dart
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import '../../widgets/custom_widgets.dart';
import '../../services/navigation_service.dart';
import '../../services/supabase_service.dart';
+import '../../services/app_shortcuts_service.dart';
import '../home/home_screen.dart';
import '../auth/set_new_password_screen.dart';
@@ -10,10 +11,10 @@ class VerifyOTPScreen extends StatefulWidget {
final String email;
final String verifyType; // 'signup_join', 'signup_create', or 'reset_password'
final Map userData;
-
+
const VerifyOTPScreen({
- super.key,
- required this.email,
+ super.key,
+ required this.email,
required this.verifyType,
this.userData = const {},
});
@@ -23,15 +24,19 @@ class VerifyOTPScreen extends StatefulWidget {
}
class _VerifyOTPScreenState extends State {
- final List _controllers = List.generate(
- 6,
- (index) => TextEditingController(),
- );
+ final List _controllers =
+ List.generate(6, (index) => TextEditingController());
final List _focusNodes = List.generate(6, (index) => FocusNode());
bool _isLoading = false;
String? _errorMessage;
final _supabaseService = SupabaseService();
+ @override
+ void initState() {
+ super.initState();
+ // Removed debug prints for production safety
+ }
+
@override
void dispose() {
for (var controller in _controllers) {
@@ -52,69 +57,96 @@ class _VerifyOTPScreenState extends State {
});
try {
- // Verify OTP with Supabase
final result = await _supabaseService.verifyOTP(
email: widget.email,
token: otp,
type: widget.verifyType,
userData: widget.userData,
);
-
+
if (result['success']) {
- // Handle successful verification based on verify type
- if (widget.verifyType == 'signup_create') {
- // Show team ID dialog for team creators
- if (result.containsKey('teamId')) {
- _showTeamIdDialog(result['teamId']);
- }
+ if (widget.verifyType == 'signup_create' && result.containsKey('teamId')) {
+ _showTeamIdDialog(result['teamId']);
} else if (widget.verifyType == 'signup_join') {
- // Navigate directly to home for team joiners
- NavigationService().navigateToReplacement(const HomeScreen());
+ _navigateToHomeScreen();
} else if (widget.verifyType == 'reset_password') {
- // Navigate to reset password screen
NavigationService().navigateTo(
SetNewPasswordScreen(email: widget.email),
);
}
} else {
setState(() {
- String errorMsg = result['error'] ?? 'Verification failed';
-
- // Make the error message more user-friendly
- if (errorMsg.contains('expired') || errorMsg.contains('otp_expired')) {
- errorMsg = 'Verification code has expired. Please request a new code.';
- } else if (errorMsg.contains('invalid')) {
- errorMsg = 'Invalid verification code. Please try again.';
- }
-
- _errorMessage = errorMsg;
+ _errorMessage = _friendlyErrorMessage(result['error']);
});
}
} catch (e) {
setState(() {
- String errorMsg = e.toString();
-
- // Make the error message more user-friendly
- if (errorMsg.contains('expired') || errorMsg.contains('otp_expired')) {
- errorMsg = 'Verification code has expired. Please request a new code.';
- } else if (errorMsg.contains('invalid')) {
- errorMsg = 'Invalid verification code. Please try again.';
- } else {
- errorMsg = 'An error occurred. Please try again.';
- }
-
- _errorMessage = errorMsg;
+ _errorMessage = _friendlyErrorMessage(e.toString());
});
} finally {
if (mounted) {
- setState(() {
- _isLoading = false;
- });
+ setState(() => _isLoading = false);
}
}
}
}
-
+
+ String _friendlyErrorMessage(String? rawMessage) {
+ if (rawMessage == null) return 'Verification failed. Please try again.';
+ if (rawMessage.contains('expired') || rawMessage.contains('otp_expired')) {
+ return 'Verification code has expired. Please request a new code.';
+ } else if (rawMessage.contains('invalid')) {
+ return 'Invalid verification code. Please try again.';
+ }
+ return 'An error occurred. Please try again.';
+ }
+
+ void _navigateToHomeScreen() {
+ final pendingShortcut = AppShortcutsService.getPendingShortcut();
+
+ // Use a copy of initial_args safely
+ Map homeScreenArgs = {};
+ final initialArgs = widget.userData['initial_args'];
+ if (initialArgs is Map) {
+ homeScreenArgs = Map.from(initialArgs);
+ }
+
+ if (pendingShortcut != null) {
+ final screenIndex = _getScreenIndex(pendingShortcut);
+ if (screenIndex != null) {
+ homeScreenArgs['screen'] = screenIndex;
+ homeScreenArgs['initial_route'] = pendingShortcut;
+ }
+ }
+
+ if (homeScreenArgs.isEmpty) {
+ homeScreenArgs = {'screen': 0};
+ }
+
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => HomeScreen(arguments: homeScreenArgs),
+ ),
+ );
+ }
+
+ int? _getScreenIndex(String? route) {
+ switch (route) {
+ case 'dashboard':
+ return 0;
+ case 'calendar':
+ return 1;
+ case 'workspace':
+ return 2;
+ case 'chat':
+ return 3;
+ case 'profile':
+ return 4;
+ default:
+ return null;
+ }
+ }
+
Future _resendCode() async {
setState(() {
_isLoading = true;
@@ -126,7 +158,7 @@ class _VerifyOTPScreenState extends State {
widget.email,
type: widget.verifyType,
);
-
+
if (result['success']) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -136,45 +168,18 @@ class _VerifyOTPScreenState extends State {
);
} else {
setState(() {
- String errorMsg = result['error'] ?? 'Failed to resend code';
-
- // Make the error message more user-friendly
- if (errorMsg.contains('Rate limit')) {
- errorMsg = 'Too many attempts. Please try again later.';
- } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) {
- errorMsg = 'Email address not found or invalid.';
- }
-
- _errorMessage = errorMsg;
+ _errorMessage = _friendlyErrorMessage(result['error']);
});
}
} catch (e) {
setState(() {
- String errorMsg = e.toString();
-
- // Make the error message more user-friendly
- if (errorMsg.contains('Rate limit')) {
- errorMsg = 'Too many attempts. Please try again later.';
- } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) {
- errorMsg = 'Email address not found or invalid.';
- } else if (errorMsg.contains('Assertion failed')) {
- errorMsg = 'Unable to resend code. Please go back and try again.';
- } else {
- errorMsg = 'An error occurred. Please try again.';
- }
-
- _errorMessage = errorMsg;
+ _errorMessage = _friendlyErrorMessage(e.toString());
});
} finally {
- if (mounted) {
- setState(() {
- _isLoading = false;
- });
- }
+ if (mounted) setState(() => _isLoading = false);
}
}
-
- // Show dialog with the generated team ID
+
void _showTeamIdDialog(String teamId) {
showDialog(
context: context,
@@ -182,20 +187,16 @@ class _VerifyOTPScreenState extends State {
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: const Color(0xFF2A2A2A),
- title: const Text(
- 'Team Created Successfully!',
- style: TextStyle(color: Colors.white),
- ),
+ title: const Text('Team Created Successfully!',
+ style: TextStyle(color: Colors.white)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
- const Text(
- 'Your Team ID is:',
- style: TextStyle(color: Colors.grey),
- ),
+ const Text('Your Team ID is:', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 16),
Container(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ padding:
+ const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(8),
@@ -206,11 +207,10 @@ class _VerifyOTPScreenState extends State {
Text(
teamId,
style: const TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.bold,
- letterSpacing: 2,
- color: Colors.white,
- ),
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 2,
+ color: Colors.white),
),
IconButton(
icon: const Icon(Icons.copy, color: Colors.green),
@@ -238,12 +238,10 @@ class _VerifyOTPScreenState extends State {
actions: [
TextButton(
onPressed: () {
- NavigationService().navigateToReplacement(const HomeScreen());
+ Navigator.of(context).pop();
+ _navigateToHomeScreen();
},
- child: Text(
- 'Continue',
- style: TextStyle(color: Colors.green.shade400),
- ),
+ child: Text('Continue', style: TextStyle(color: Colors.green.shade400)),
),
],
);
@@ -336,4 +334,4 @@ class _VerifyOTPScreenState extends State {
],
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart
index 46fc229..6d2d543 100644
--- a/lib/screens/home/home_screen.dart
+++ b/lib/screens/home/home_screen.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../widgets/custom_widgets.dart';
import '../../services/navigation_service.dart';
+import '../../services/app_shortcuts_service.dart';
import '../workspace/workspace_screen.dart';
import '../calendar/calendar_screen.dart';
import '../profile/profile_screen.dart';
@@ -10,7 +11,7 @@ import 'dashboard_screen.dart';
class HomeScreen extends StatefulWidget {
final Map? arguments;
-
+
const HomeScreen({super.key, this.arguments});
@override
@@ -28,11 +29,12 @@ class _HomeScreenState extends State
late AnimationController _fabController;
late Animation _fabAnimation;
- List _screens = [];
+ late final List _screens;
@override
void initState() {
super.initState();
+
_fabController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
@@ -42,35 +44,29 @@ class _HomeScreenState extends State
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
-
- // Initialize screens
+
_initializeScreens();
-
- // Handle initial arguments if provided
+
+ // Handle initial arguments safely
WidgetsBinding.instance.addPostFrameCallback((_) {
- if (widget.arguments != null) {
- if (widget.arguments!.containsKey('screen') && widget.arguments!['screen'] is int) {
- setState(() {
- _selectedIndex = widget.arguments!['screen'];
- });
- }
-
- // Handle initial message for chat screen
- if (widget.arguments!.containsKey('initial_message') &&
- widget.arguments!['initial_message'] is String &&
- _selectedIndex == 3) {
- // Update the chat screen with the initial message
- setState(() {
- _screens[3] = ChatScreen(
- arguments: {'initial_message': widget.arguments!['initial_message']}
- );
- });
+ final args = widget.arguments;
+ if (args != null) {
+ if (args['screen'] is int) _selectedIndex = args['screen'];
+ if (args['initial_message'] is String && _selectedIndex == 3) {
+ _screens[3] = ChatScreen(
+ arguments: {'initial_message': args['initial_message']},
+ );
}
+ setState(() {});
}
});
+
+ // Initialize AppShortcutsService
+ AppShortcutsService.init(_handleShortcut);
}
-
+
void _initializeScreens() {
+ // Initialize screens once to prevent unnecessary rebuilds
_screens = [
const DashboardScreen(),
const CalendarScreen(),
@@ -80,6 +76,33 @@ class _HomeScreenState extends State
];
}
+ void _handleShortcut(String route) {
+ int index;
+ switch (route) {
+ case 'dashboard':
+ index = 0;
+ break;
+ case 'calendar':
+ index = 1;
+ break;
+ case 'workspace':
+ index = 2;
+ break;
+ case 'chat':
+ index = 3;
+ // Refresh chat screen if needed
+ _screens[3] = ChatScreen();
+ break;
+ case 'profile':
+ index = 4;
+ break;
+ default:
+ index = 0;
+ }
+
+ if (mounted) setState(() => _selectedIndex = index);
+ }
+
@override
void dispose() {
_messageController.dispose();
@@ -89,21 +112,19 @@ class _HomeScreenState extends State
}
void _sendMessage() {
- if (_messageController.text.trim().isEmpty) return;
+ final text = _messageController.text.trim();
+ if (text.isEmpty) return;
setState(() {
- _messages.add(
- ChatMessage(
- text: _messageController.text,
- isUser: true,
- timestamp: DateTime.now(),
- ),
- );
+ _messages.add(ChatMessage(
+ text: text,
+ isUser: true,
+ timestamp: DateTime.now(),
+ ));
_messageController.clear();
});
_scrollToBottom();
- // TODO: Implement AI response
}
void _scrollToBottom() {
@@ -119,10 +140,7 @@ class _HomeScreenState extends State
}
void _toggleListening() {
- setState(() {
- _isListening = !_isListening;
- });
- // TODO: Implement voice recognition
+ setState(() => _isListening = !_isListening);
}
void _toggleFab() {
@@ -157,18 +175,25 @@ class _HomeScreenState extends State
icon: Icon(Icons.calendar_today),
label: 'Calendar',
),
- BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Workspace'),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.work),
+ label: 'Workspace',
+ ),
BottomNavigationBarItem(
icon: Icon(Icons.chat_bubble_outline),
label: 'Chat',
),
- BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.person),
+ label: 'Profile',
+ ),
],
),
);
}
}
+// Chat bubble widget
class _ChatBubble extends StatelessWidget {
final ChatMessage message;
@@ -177,12 +202,14 @@ class _ChatBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Align(
- alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
+ alignment:
+ message.isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
- color: message.isUser ? Colors.green.shade400 : Colors.grey.shade800,
+ color:
+ message.isUser ? Colors.green.shade400 : Colors.grey.shade800,
borderRadius: BorderRadius.circular(20),
),
child: Text(message.text, style: const TextStyle(color: Colors.white)),
@@ -191,6 +218,7 @@ class _ChatBubble extends StatelessWidget {
}
}
+// Chat message model
class ChatMessage {
final String text;
final bool isUser;
diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart
index b20bb00..a456256 100644
--- a/lib/screens/splash_screen.dart
+++ b/lib/screens/splash_screen.dart
@@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'dart:async';
+
import 'onboarding/onboarding_screen.dart';
import '../services/navigation_service.dart';
import '../services/supabase_service.dart';
+import '../services/app_shortcuts_service.dart';
import 'home/home_screen.dart';
import 'auth/login_screen.dart';
class SplashScreen extends StatefulWidget {
- const SplashScreen({super.key});
+ final Map? arguments;
+
+ const SplashScreen({super.key, this.arguments});
@override
State createState() => _SplashScreenState();
@@ -23,6 +27,9 @@ class _SplashScreenState extends State
@override
void initState() {
super.initState();
+
+ debugPrint('[SplashScreen] initState called with arguments: ${widget.arguments}');
+
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
@@ -38,40 +45,48 @@ class _SplashScreenState extends State
_animationController.forward();
- Timer(const Duration(milliseconds: 1500), () {
- _checkSession();
- });
+ // Give a small delay to ensure everything is initialized
+ Timer(const Duration(milliseconds: 1500), _checkSession);
}
Future _checkSession() async {
try {
final currentUser = _supabaseService.client.auth.currentUser;
- final args = ModalRoute.of(context)?.settings.arguments;
+ // Get initial shortcut from AppShortcutsService
+ final initialShortcut = AppShortcutsService.getPendingShortcut();
+ debugPrint('[SplashScreen] Initial shortcut: $initialShortcut');
+
+ // Convert shortcut to screen index
+ int? screenIndex = _getScreenIndex(initialShortcut);
+ debugPrint('[SplashScreen] Converted screen index: $screenIndex');
+
+ // Create a copy of arguments to avoid mutating potentially unmodifiable map
+ Map homeScreenArgs = Map.from(widget.arguments ?? {});
+ if (screenIndex != null) {
+ homeScreenArgs['screen'] = screenIndex;
+ homeScreenArgs['initial_route'] = initialShortcut;
+ }
+
+ debugPrint('[SplashScreen] HomeScreen arguments: $homeScreenArgs');
if (currentUser != null) {
- if (args != null && args is Map) {
- Navigator.of(context).pushReplacement(
- MaterialPageRoute(
- builder: (context) => HomeScreen(arguments: args),
- ),
- );
- } else {
- Navigator.of(context).pushReplacement(
- MaterialPageRoute(
- builder: (context) => const HomeScreen(),
- ),
- );
- }
+ // User is logged in, go to HomeScreen
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => HomeScreen(arguments: homeScreenArgs),
+ ),
+ );
} else {
+ // User is not logged in, go to LoginScreen
Navigator.of(context).pushReplacement(
MaterialPageRoute(
- builder: (context) => const LoginScreen(),
+ builder: (context) => LoginScreen(arguments: homeScreenArgs),
),
);
}
} catch (e) {
- debugPrint('Error checking session: $e');
+ debugPrint('[SplashScreen] Error checking session: $e');
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const OnboardingScreen(),
@@ -80,6 +95,25 @@ class _SplashScreenState extends State
}
}
+ int? _getScreenIndex(String? route) {
+ if (route == null) return null;
+
+ switch (route) {
+ case 'dashboard':
+ return 0;
+ case 'calendar':
+ return 1;
+ case 'workspace':
+ return 2;
+ case 'chat':
+ return 3;
+ case 'profile':
+ return 4;
+ default:
+ return null;
+ }
+ }
+
@override
void dispose() {
_animationController.dispose();
@@ -108,10 +142,7 @@ class _SplashScreenState extends State
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
- colors: [
- Colors.green.shade400,
- Colors.green.shade700
- ],
+ colors: [Colors.green.shade400, Colors.green.shade700],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@@ -123,11 +154,7 @@ class _SplashScreenState extends State
),
],
),
- child: const Icon(
- Icons.task_alt,
- size: 80,
- color: Colors.white,
- ),
+ child: const Icon(Icons.task_alt, size: 80, color: Colors.white),
);
},
),
diff --git a/lib/services/app_shortcuts_service.dart b/lib/services/app_shortcuts_service.dart
new file mode 100644
index 0000000..e278228
--- /dev/null
+++ b/lib/services/app_shortcuts_service.dart
@@ -0,0 +1,113 @@
+import 'package:flutter/services.dart';
+
+class AppShortcutsService {
+ AppShortcutsService._();
+
+ static const MethodChannel _channel = MethodChannel('app_shortcuts');
+ static String? _pendingShortcut;
+ static Function(String route)? _onShortcutPressed;
+
+ /// Initialize the shortcuts service and get initial shortcut if any
+ static Future initialize() async {
+ try {
+ // Get initial shortcut when app launches
+ final initialRoute = await _channel.invokeMethod('getInitialRoute');
+ if (initialRoute != null && initialRoute.isNotEmpty) {
+ _pendingShortcut = initialRoute;
+ }
+ } catch (e) {
+ print('Error initializing shortcuts service: $e');
+ }
+ }
+
+ /// Get and clear pending shortcut (for initial app launch)
+ static String? getPendingShortcut() {
+ final shortcut = _pendingShortcut;
+ _pendingShortcut = null;
+ return shortcut;
+ }
+
+ /// Initialize shortcut handler for in-app navigation
+ static void init(void Function(String route) onShortcutPressed) {
+ _onShortcutPressed = onShortcutPressed;
+
+ _channel.setMethodCallHandler((call) async {
+ if (call.method == 'navigate') {
+ final String route = call.arguments?.toString() ?? '';
+ if (route.isNotEmpty) {
+ _handleShortcut(route);
+ }
+ }
+ return null;
+ });
+ }
+
+ /// Handle shortcut route and convert to index for backward compatibility
+ static void initWithIndex(void Function(int index) changeTab) {
+ init((route) {
+ int index = _routeToIndex(route);
+ changeTab(index);
+ });
+ }
+
+ /// Convert route string to tab index
+ static int _routeToIndex(String route) {
+ switch (route) {
+ case 'dashboard': return 0;
+ case 'calendar': return 1;
+ case 'workspace': return 2;
+ case 'chat': return 3;
+ case 'profile': return 4;
+ default: return 0;
+ }
+ }
+
+ /// Convert tab index to route string
+ static String _indexToRoute(int index) {
+ switch (index) {
+ case 0: return 'dashboard';
+ case 1: return 'calendar';
+ case 2: return 'workspace';
+ case 3: return 'chat';
+ case 4: return 'profile';
+ default: return 'dashboard';
+ }
+ }
+
+ /// Handle shortcut internally
+ static void _handleShortcut(String route) {
+ if (_onShortcutPressed != null) {
+ _onShortcutPressed!(route);
+ }
+ }
+
+ /// Manually trigger navigation (for testing or programmatic navigation)
+ static void navigateTo(String route) {
+ _handleShortcut(route);
+ }
+
+ /// Navigate to specific tab index
+ static void navigateToIndex(int index) {
+ final route = _indexToRoute(index);
+ _handleShortcut(route);
+ }
+
+ /// Check if there's a pending shortcut
+ static bool hasPendingShortcut() {
+ return _pendingShortcut != null && _pendingShortcut!.isNotEmpty;
+ }
+
+ /// Clear any pending shortcuts
+ static void clearPendingShortcut() {
+ _pendingShortcut = null;
+ }
+
+ /// Send a shortcut to native (for testing or cross-platform communication)
+ static Future sendShortcutToNative(String route) async {
+ try {
+ await _channel.invokeMethod('navigate', route);
+ } catch (e) {
+ print('Error sending shortcut to native: $e');
+ }
+ }
+}
\ No newline at end of file
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 3792af4..1a9d148 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -7,12 +7,16 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
+ g_autoptr(FlPluginRegistrar) printing_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
+ printing_plugin_register_with_registrar(printing_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 5d07423..1db2f43 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
gtk
+ printing
url_launcher_linux
)
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 92b6497..145cd5f 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -7,12 +7,16 @@ import Foundation
import app_links
import path_provider_foundation
+import printing
import shared_preferences_foundation
+import speech_to_text
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+ SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/pubspec.lock b/pubspec.lock
index 2d3a236..7bb4dec 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -958,5 +958,5 @@ packages:
source: hosted
version: "2.1.0"
sdks:
- dart: ">=3.8.0 <=3.8.1"
- flutter: ">=3.32.0"
+ dart: ">=3.7.0 <=3.8.1"
+ flutter: ">=3.31.0-0.0.pre"
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 785a046..a16341e 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -7,11 +7,20 @@
#include "generated_plugin_registrant.h"
#include
+#include
+#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
+ PrintingPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PrintingPlugin"));
+ SpeechToTextWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("SpeechToTextWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 8f8ee4f..2d82e75 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
+ permission_handler_windows
+ printing
+ speech_to_text_windows
url_launcher_windows
)