Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ app.*.map.json
lib/services/local_config.dart
*.env
server/goworkbro-server.exe

# Python cache
__pycache__/
*.pyc
Binary file added assets/fonts/LXGWWenKai-Regular.ttf
Binary file not shown.
Binary file added assets/icons/app_icon.ico
Binary file not shown.
Binary file added assets/icons/app_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/tray_icon.ico
Binary file not shown.
60 changes: 49 additions & 11 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:provider/provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:window_manager/window_manager.dart';
import 'providers/app_provider.dart';
import 'services/app_locale.dart';
import 'services/supabase_config.dart';
import 'services/tray_service.dart';
import 'theme/app_theme.dart';
import 'screens/auth_screen.dart';
import 'screens/todo_screen.dart';
Expand All @@ -15,6 +17,7 @@ import 'screens/me_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();

// Must be initialized before window_manager on Windows
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
await windowManager.ensureInitialized();
await windowManager.waitUntilReadyToShow(
Expand All @@ -30,13 +33,17 @@ void main() async {
await windowManager.focus();
},
);
// Prevent default close — we hide to tray instead
await windowManager.setPreventClose(true);
// Initialize system tray
await TrayService().init();
}

// Initialize Supabase before running app (if configured)
if (isSupabaseConfigured) {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey,
publishableKey: supabaseAnonKey,
debug: kDebugMode,
);
}
Expand All @@ -49,15 +56,28 @@ class GoWorkBroApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AppProvider(),
child: MaterialApp(
title: 'GoWorkBro',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
home: const AuthGate(),
return MultiProvider(
providers: [
ChangeNotifierProvider<AppLocaleProvider>(
create: (_) => AppLocaleProvider(),
),
ChangeNotifierProvider<AppProvider>(
create: (_) => AppProvider(),
),
],
child: Consumer<AppLocaleProvider>(
builder: (context, localeProvider, _) {
return MaterialApp(
title: 'GoWorkBro',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: localeProvider.themeMode,
locale: localeProvider.flutterLocale,
supportedLocales: const [Locale('zh'), Locale('en')],
home: const AuthGate(),
);
},
),
);
}
Expand Down Expand Up @@ -145,9 +165,27 @@ class AppShell extends StatefulWidget {
State<AppShell> createState() => _AppShellState();
}

class _AppShellState extends State<AppShell> {
class _AppShellState extends State<AppShell> with WindowListener {
int _currentIndex = 0;

@override
void initState() {
super.initState();
windowManager.addListener(this);
}

@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}

/// Intercept the window close button — hide to tray instead of quitting.
@override
void onWindowClose() async {
await windowManager.hide();
}

final _screens = const [
TodoScreen(),
CountdownScreen(),
Expand Down
8 changes: 8 additions & 0 deletions lib/models/sleep_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,47 @@ class SleepRecord {
final String recordDate;
final String? wakeTime;
final String? sleepTime;
final String? workoutTime;
final String? note;

SleepRecord({
required this.id,
required this.recordDate,
this.wakeTime,
this.sleepTime,
this.workoutTime,
this.note,
});

factory SleepRecord.create({
required String recordDate,
String? wakeTime,
String? sleepTime,
String? workoutTime,
String? note,
}) {
return SleepRecord(
id: _uuid.v4(),
recordDate: recordDate,
wakeTime: wakeTime,
sleepTime: sleepTime,
workoutTime: workoutTime,
note: note,
);
}

SleepRecord copyWith({
String? wakeTime,
String? sleepTime,
String? workoutTime,
String? note,
}) {
return SleepRecord(
id: id,
recordDate: recordDate,
wakeTime: wakeTime ?? this.wakeTime,
sleepTime: sleepTime ?? this.sleepTime,
workoutTime: workoutTime ?? this.workoutTime,
note: note ?? this.note,
);
}
Expand All @@ -51,6 +57,7 @@ class SleepRecord {
'record_date': recordDate,
'wake_time': wakeTime,
'sleep_time': sleepTime,
'workout_time': workoutTime,
'note': note,
};

Expand All @@ -59,6 +66,7 @@ class SleepRecord {
recordDate: m['record_date'] as String,
wakeTime: m['wake_time'] as String?,
sleepTime: m['sleep_time'] as String?,
workoutTime: m['workout_time'] as String?,
note: m['note'] as String?,
);
}
100 changes: 86 additions & 14 deletions lib/providers/app_provider.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/models.dart';
import '../services/database_service.dart';
import '../services/api_service.dart';
import '../services/app_locale.dart';
import '../services/sync_service.dart';
import '../services/supabase_config.dart';

const _uuid = Uuid();

/// Central app state — manages all data and daily rollover logic
class AppProvider extends ChangeNotifier {
List<Todo> _todos = [];
Expand All @@ -16,8 +19,8 @@ class AppProvider extends ChangeNotifier {
List<FocusSession> _todaySessions = [];
List<SleepRecord> _sleepRecords = [];
String _userName = 'AzarAI';
String _apiBaseUrl = 'http://localhost:8765';
bool _apiConnected = false;
AppLocale _locale = AppLocale.zh;
ThemeMode _themeMode = ThemeMode.system;
String _lastRolloverDate = '';
Timer? _rolloverTimer;

Expand All @@ -27,8 +30,11 @@ class AppProvider extends ChangeNotifier {
List<FocusSession> get todaySessions => _todaySessions;
List<SleepRecord> get sleepRecords => _sleepRecords;
String get userName => _userName;
String get apiBaseUrl => _apiBaseUrl;
bool get apiConnected => _apiConnected;
AppLocale get locale => _locale;
ThemeMode get themeMode => _themeMode;
Locale get flutterLocale => _locale == AppLocale.zh
? const Locale('zh')
: const Locale('en');

String get todayDate {
final now = DateTime.now();
Expand All @@ -37,8 +43,26 @@ class AppProvider extends ChangeNotifier {

Future<void> init() async {
_userName = await DatabaseService.getSetting('user_name') ?? 'AzarAI';
_apiBaseUrl = await DatabaseService.getSetting('api_base_url') ??
'http://localhost:8765';

// Load persisted locale and theme mode
final savedLocale = await DatabaseService.getSetting('locale');
if (savedLocale == 'en') {
_locale = AppLocale.en;
} else {
_locale = AppLocale.zh;
}
final savedTheme = await DatabaseService.getSetting('theme_mode');
switch (savedTheme) {
case 'light':
_themeMode = ThemeMode.light;
break;
case 'dark':
_themeMode = ThemeMode.dark;
break;
default:
_themeMode = ThemeMode.system;
}

_lastRolloverDate =
await DatabaseService.getSetting('last_rollover_date') ?? '';

Expand All @@ -57,14 +81,15 @@ class AppProvider extends ChangeNotifier {
}
}

_apiConnected = await ApiService.testConnection();

// Check every minute if the date has changed (for cross-day rollover)
_rolloverTimer = Timer.periodic(const Duration(minutes: 1), (_) {
if (_lastRolloverDate != todayDate) {
_performDailyRollover().then((_) => refreshAll());
}
});

_isInitialized = true;
notifyListeners();
}

@override
Expand Down Expand Up @@ -116,6 +141,24 @@ class AppProvider extends ChangeNotifier {
completedDate: !todo.isCompleted ? DateTime.now().toIso8601String() : null,
);
await DatabaseService.updateTodo(updated);

// "明天继续": when marking complete and the todo wants to repeat tomorrow,
// auto-recreate an incomplete copy for the next day.
if (!todo.isCompleted && todo.keepTomorrow) {
final newTodo = Todo(
id: _uuid.v4(),
title: todo.title,
timingType: todo.timingType,
durationMinutes: todo.durationMinutes,
isCompleted: false,
sortOrder: todo.sortOrder,
keepTomorrow: true,
createdDate: DateTime.now().toIso8601String(),
);
await DatabaseService.insertTodo(newTodo);
SyncService.pushTodo(newTodo);
}

_todos = await DatabaseService.getTodos();
notifyListeners();
SyncService.pushTodo(updated);
Expand Down Expand Up @@ -247,10 +290,39 @@ class AppProvider extends ChangeNotifier {
notifyListeners();
}

Future<void> setApiBaseUrl(String url) async {
_apiBaseUrl = url;
await ApiService.setBaseUrl(url);
_apiConnected = await ApiService.testConnection();
Future<void> setLocale(AppLocale locale) async {
_locale = locale;
await DatabaseService.setSetting('locale', locale == AppLocale.en ? 'en' : 'zh');
notifyListeners();
}

Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
String value;
switch (mode) {
case ThemeMode.light:
value = 'light';
break;
case ThemeMode.dark:
value = 'dark';
break;
case ThemeMode.system:
value = 'system';
break;
}
await DatabaseService.setSetting('theme_mode', value);
notifyListeners();
}

/// Delete all local data and reset in-memory state.
Future<void> deleteAllData() async {
await DatabaseService.deleteAllData();
_todos = [];
_habits = [];
_countdowns = [];
_todaySessions = [];
_sleepRecords = [];
_lastRolloverDate = '';
notifyListeners();
}

Expand Down
Loading
Loading