first commit

This commit is contained in:
ytc1012
2025-11-13 15:45:28 +08:00
commit 6b321890c0
54 changed files with 8412 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../models/app_usage.dart';
import 'database_helper.dart';
class AppUsageDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// Web 平台检查
bool get _isWeb => kIsWeb;
// 插入应用使用记录
Future<int> insertAppUsage(AppUsage usage) async {
final db = await _dbHelper.database;
return await db.insert('app_usage', usage.toMap());
}
// 批量插入
Future<void> batchInsertAppUsages(List<AppUsage> usages) async {
final db = await _dbHelper.database;
final batch = db.batch();
for (final usage in usages) {
batch.insert('app_usage', usage.toMap());
}
await batch.commit(noResult: true);
}
// 获取指定时间范围的应用使用记录
Future<List<AppUsage>> getAppUsages({
required DateTime startTime,
required DateTime endTime,
}) async {
if (_isWeb) return [];
final db = await _dbHelper.database;
final startTimestamp = startTime.millisecondsSinceEpoch ~/ 1000;
final endTimestamp = endTime.millisecondsSinceEpoch ~/ 1000;
final maps = await db.query(
'app_usage',
where: 'start_time >= ? AND end_time <= ?',
whereArgs: [startTimestamp, endTimestamp],
orderBy: 'start_time DESC',
);
return maps.map((map) {
return AppUsage(
id: map['id'] as int?,
packageName: map['package_name'] as String,
appName: map['app_name'] as String,
startTime: DateTime.fromMillisecondsSinceEpoch((map['start_time'] as int) * 1000),
endTime: DateTime.fromMillisecondsSinceEpoch((map['end_time'] as int) * 1000),
duration: map['duration'] as int,
category: map['category'] as String,
projectId: map['project_id'] as int?,
deviceUnlockCount: map['device_unlock_count'] as int,
createdAt: DateTime.fromMillisecondsSinceEpoch((map['created_at'] as int) * 1000),
updatedAt: DateTime.fromMillisecondsSinceEpoch((map['updated_at'] as int) * 1000),
);
}).toList();
}
// 获取今日应用使用记录
Future<List<AppUsage>> getTodayAppUsages() async {
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
return await getAppUsages(startTime: startOfDay, endTime: endOfDay);
}
// 获取 Top N 应用
Future<List<AppUsage>> getTopApps({
required DateTime startTime,
required DateTime endTime,
int limit = 10,
}) async {
if (_isWeb) return [];
final db = await _dbHelper.database;
final startTimestamp = startTime.millisecondsSinceEpoch ~/ 1000;
final endTimestamp = endTime.millisecondsSinceEpoch ~/ 1000;
final maps = await db.rawQuery('''
SELECT
package_name,
app_name,
category,
SUM(duration) as total_duration,
MIN(start_time) as first_start_time,
MAX(end_time) as last_end_time,
MIN(created_at) as created_at,
MAX(updated_at) as updated_at
FROM app_usage
WHERE start_time >= ? AND end_time <= ?
GROUP BY package_name, app_name, category
ORDER BY total_duration DESC
LIMIT ?
''', [startTimestamp, endTimestamp, limit]);
return maps.map((map) {
final firstStart = DateTime.fromMillisecondsSinceEpoch((map['first_start_time'] as int) * 1000);
final lastEnd = DateTime.fromMillisecondsSinceEpoch((map['last_end_time'] as int) * 1000);
return AppUsage(
packageName: map['package_name'] as String,
appName: map['app_name'] as String,
startTime: firstStart,
endTime: lastEnd,
duration: map['total_duration'] as int,
category: map['category'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch((map['created_at'] as int) * 1000),
updatedAt: DateTime.fromMillisecondsSinceEpoch((map['updated_at'] as int) * 1000),
);
}).toList();
}
// 更新应用分类
Future<void> updateAppUsageCategory(int id, String category) async {
final db = await _dbHelper.database;
await db.update(
'app_usage',
{
'category': category,
'updated_at': DateTime.now().millisecondsSinceEpoch ~/ 1000,
},
where: 'id = ?',
whereArgs: [id],
);
}
// 删除应用使用记录
Future<void> deleteAppUsage(int id) async {
if (_isWeb) return;
final db = await _dbHelper.database;
await db.delete(
'app_usage',
where: 'id = ?',
whereArgs: [id],
);
}
// 删除指定日期之前的数据(用于数据清理)
Future<void> deleteBeforeDate(DateTime date) async {
if (_isWeb) return;
final db = await _dbHelper.database;
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
await db.delete(
'app_usage',
where: 'start_time < ?',
whereArgs: [timestamp],
);
}
}

View File

@@ -0,0 +1,165 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../models/daily_stats.dart';
import 'database_helper.dart';
class DailyStatsDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// Web 平台检查
bool get _isWeb => kIsWeb;
// 插入或更新每日统计
Future<void> upsertDailyStats(DailyStats stats) async {
final db = await _dbHelper.database;
final dateStr = _formatDate(stats.date);
final map = stats.toMap();
final existing = await db.query(
'daily_stats',
where: 'date = ?',
whereArgs: [dateStr],
);
if (existing.isEmpty) {
await db.insert('daily_stats', map);
} else {
await db.update(
'daily_stats',
map,
where: 'date = ?',
whereArgs: [dateStr],
);
}
}
// 获取指定日期的统计
Future<DailyStats?> getDailyStats(DateTime date) async {
final db = await _dbHelper.database;
final dateStr = _formatDate(date);
final maps = await db.query(
'daily_stats',
where: 'date = ?',
whereArgs: [dateStr],
);
if (maps.isEmpty) return null;
return _mapToDailyStats(maps.first);
}
// 获取今日统计
Future<DailyStats?> getTodayStats() async {
return await getDailyStats(DateTime.now());
}
// 获取指定日期范围的统计
Future<List<DailyStats>> getStatsRange({
required DateTime startDate,
required DateTime endDate,
}) async {
if (_isWeb) return [];
final db = await _dbHelper.database;
final startStr = _formatDate(startDate);
final endStr = _formatDate(endDate);
final maps = await db.query(
'daily_stats',
where: 'date >= ? AND date <= ?',
whereArgs: [startStr, endStr],
orderBy: 'date ASC',
);
return maps.map((map) => _mapToDailyStats(map)).toList();
}
// 获取本周统计
Future<List<DailyStats>> getWeekStats() async {
final now = DateTime.now();
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
return await getStatsRange(startDate: startOfWeek, endDate: endOfWeek);
}
// 获取本月统计
Future<List<DailyStats>> getMonthStats() async {
final now = DateTime.now();
final startOfMonth = DateTime(now.year, now.month, 1);
final endOfMonth = DateTime(now.year, now.month + 1, 0);
return await getStatsRange(startDate: startOfMonth, endDate: endOfMonth);
}
// 删除指定日期之前的数据
Future<void> deleteBeforeDate(DateTime date) async {
if (_isWeb) return;
final db = await _dbHelper.database;
final dateStr = _formatDate(date);
await db.delete(
'daily_stats',
where: 'date < ?',
whereArgs: [dateStr],
);
}
// 删除指定 ID 的统计
Future<void> deleteDailyStats(int id) async {
if (_isWeb) return;
final db = await _dbHelper.database;
await db.delete(
'daily_stats',
where: 'id = ?',
whereArgs: [id],
);
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
DailyStats _mapToDailyStats(Map<String, dynamic> map) {
return DailyStats(
id: map['id'] as int?,
date: DateTime.parse(map['date'] as String),
totalTime: map['total_time'] as int,
workTime: map['work_time'] as int? ?? 0,
studyTime: map['study_time'] as int? ?? 0,
entertainmentTime: map['entertainment_time'] as int? ?? 0,
socialTime: map['social_time'] as int? ?? 0,
toolTime: map['tool_time'] as int? ?? 0,
efficiencyScore: map['efficiency_score'] as int?,
focusScore: map['focus_score'] as int?,
appSwitchCount: map['app_switch_count'] as int? ?? 0,
createdAt: DateTime.fromMillisecondsSinceEpoch((map['created_at'] as int) * 1000),
updatedAt: DateTime.fromMillisecondsSinceEpoch((map['updated_at'] as int) * 1000),
);
}
}
// 扩展 DailyStats 模型,添加 toMap 方法
extension DailyStatsExtension on DailyStats {
Map<String, dynamic> toMap() {
return {
'id': id,
'date': _formatDate(date),
'total_time': totalTime,
'work_time': workTime,
'study_time': studyTime,
'entertainment_time': entertainmentTime,
'social_time': socialTime,
'tool_time': toolTime,
'efficiency_score': efficiencyScore,
'focus_score': focusScore,
'app_switch_count': appSwitchCount,
'created_at': createdAt.millisecondsSinceEpoch ~/ 1000,
'updated_at': updatedAt.millisecondsSinceEpoch ~/ 1000,
};
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}

View File

@@ -0,0 +1,114 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
// Web 平台不支持 sqflite
if (kIsWeb) {
throw UnsupportedError('SQLite is not supported on Web platform. Use mock data instead.');
}
if (_database != null) return _database!;
_database = await _initDB('autotime_tracker.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
// Web 平台不支持 sqflite
if (kIsWeb) {
throw UnsupportedError('SQLite is not supported on Web platform.');
}
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
Future<void> _createDB(Database db, int version) async {
// 应用使用记录表
await db.execute('''
CREATE TABLE app_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
package_name TEXT NOT NULL,
app_name TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
duration INTEGER NOT NULL,
category TEXT NOT NULL,
project_id INTEGER,
device_unlock_count INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
// 每日统计表
await db.execute('''
CREATE TABLE daily_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL UNIQUE,
total_time INTEGER NOT NULL,
work_time INTEGER DEFAULT 0,
study_time INTEGER DEFAULT 0,
entertainment_time INTEGER DEFAULT 0,
social_time INTEGER DEFAULT 0,
tool_time INTEGER DEFAULT 0,
efficiency_score INTEGER,
focus_score INTEGER,
app_switch_count INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
// 应用分类表
await db.execute('''
CREATE TABLE app_category (
id INTEGER PRIMARY KEY AUTOINCREMENT,
package_name TEXT NOT NULL UNIQUE,
category TEXT NOT NULL,
is_custom INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
// 时间目标表
await db.execute('''
CREATE TABLE time_goal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
goal_type TEXT NOT NULL,
category TEXT,
target_time INTEGER NOT NULL,
is_active INTEGER DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
// 创建索引
await db.execute('CREATE INDEX idx_app_usage_date ON app_usage(start_time)');
await db.execute('CREATE INDEX idx_app_usage_category ON app_usage(category)');
await db.execute('CREATE INDEX idx_app_usage_package ON app_usage(package_name)');
await db.execute('CREATE INDEX idx_daily_stats_date ON daily_stats(date)');
await db.execute('CREATE INDEX idx_app_category_package ON app_category(package_name)');
}
// 关闭数据库
Future<void> close() async {
final db = await database;
await db.close();
}
}

View File

@@ -0,0 +1,130 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../models/time_goal.dart';
import 'database_helper.dart';
class TimeGoalDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// Web 平台使用内存存储
final List<TimeGoal> _webGoals = [];
// Web 平台检查
bool get _isWeb => kIsWeb;
// 插入或更新时间目标
Future<void> upsertTimeGoal(TimeGoal goal) async {
if (_isWeb) {
// Web 平台使用内存存储
final index = _webGoals.indexWhere((g) =>
g.goalType == goal.goalType && g.category == goal.category);
if (index >= 0) {
_webGoals[index] = goal;
} else {
_webGoals.add(goal);
}
return;
}
final db = await _dbHelper.database;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final map = {
'goal_type': goal.goalType,
'category': goal.category,
'target_time': goal.targetTime,
'is_active': goal.isActive ? 1 : 0,
'created_at': now,
'updated_at': now,
};
// 检查是否已存在相同类型的目标
final existing = await db.query(
'time_goal',
where: 'goal_type = ? AND (category = ? OR category IS NULL)',
whereArgs: [
goal.goalType,
goal.category,
],
);
if (existing.isEmpty) {
await db.insert('time_goal', map);
} else {
await db.update(
'time_goal',
map,
where: 'goal_type = ? AND (category = ? OR category IS NULL)',
whereArgs: [
goal.goalType,
goal.category,
],
);
}
}
// 获取所有激活的目标
Future<List<TimeGoal>> getActiveGoals() async {
if (_isWeb) {
return _webGoals.where((g) => g.isActive).toList();
}
final db = await _dbHelper.database;
final maps = await db.query(
'time_goal',
where: 'is_active = 1',
orderBy: 'goal_type, category',
);
return maps.map((map) => TimeGoal.fromMap(map)).toList();
}
// 获取所有目标(包括非激活的)
Future<List<TimeGoal>> getAllGoals() async {
if (_isWeb) {
return List<TimeGoal>.from(_webGoals);
}
final db = await _dbHelper.database;
final maps = await db.query(
'time_goal',
orderBy: 'goal_type, category',
);
return maps.map((map) => TimeGoal.fromMap(map)).toList();
}
// 获取指定类型的目标
Future<TimeGoal?> getGoal(String goalType, {String? category}) async {
final db = await _dbHelper.database;
final maps = await db.query(
'time_goal',
where: 'goal_type = ? AND (category = ? OR category IS NULL) AND is_active = 1',
whereArgs: [goalType, category],
);
if (maps.isEmpty) return null;
return TimeGoal.fromMap(maps.first);
}
// 删除目标
Future<void> deleteGoal(int id) async {
final db = await _dbHelper.database;
await db.delete(
'time_goal',
where: 'id = ?',
whereArgs: [id],
);
}
// 停用目标
Future<void> deactivateGoal(int id) async {
final db = await _dbHelper.database;
await db.update(
'time_goal',
{'is_active': 0, 'updated_at': DateTime.now().millisecondsSinceEpoch ~/ 1000},
where: 'id = ?',
whereArgs: [id],
);
}
}

84
lib/main.dart Normal file
View File

@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'screens/home_screen.dart';
import 'screens/permission_screen.dart';
import 'theme/app_theme.dart';
import 'providers/time_tracking_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化日期格式化本地化数据
await initializeDateFormatting('zh_CN', null);
runApp(
const ProviderScope(
child: AutoTimeTrackerApp(),
),
);
}
class AutoTimeTrackerApp extends StatelessWidget {
const AutoTimeTrackerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AutoTime Tracker',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const PermissionCheckScreen(),
);
}
}
/// 权限检查屏幕 - 检查权限后决定显示哪个页面
class PermissionCheckScreen extends ConsumerWidget {
const PermissionCheckScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Web 平台直接显示主界面(使用测试数据)
if (kIsWeb) {
return const HomeScreen();
}
final permissionStatus = ref.watch(permissionStatusProvider);
return permissionStatus.when(
data: (hasPermission) {
if (hasPermission) {
return const HomeScreen();
} else {
return const PermissionScreen();
}
},
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('检查权限时出错: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.invalidate(permissionStatusProvider);
},
child: const Text('重试'),
),
],
),
),
),
);
}
}

70
lib/models/app_usage.dart Normal file
View File

@@ -0,0 +1,70 @@
class AppUsage {
final int? id;
final String packageName;
final String appName;
final DateTime startTime;
final DateTime endTime;
final int duration; // 秒
final String category;
final int? projectId;
final int deviceUnlockCount;
final DateTime createdAt;
final DateTime updatedAt;
AppUsage({
this.id,
required this.packageName,
required this.appName,
required this.startTime,
required this.endTime,
required this.duration,
required this.category,
this.projectId,
this.deviceUnlockCount = 0,
required this.createdAt,
required this.updatedAt,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'package_name': packageName,
'app_name': appName,
'start_time': startTime.millisecondsSinceEpoch ~/ 1000,
'end_time': endTime.millisecondsSinceEpoch ~/ 1000,
'duration': duration,
'category': category,
'project_id': projectId,
'device_unlock_count': deviceUnlockCount,
'created_at': createdAt.millisecondsSinceEpoch ~/ 1000,
'updated_at': updatedAt.millisecondsSinceEpoch ~/ 1000,
};
}
factory AppUsage.fromMap(Map<String, dynamic> map) {
return AppUsage(
id: map['id'],
packageName: map['package_name'],
appName: map['app_name'],
startTime: DateTime.fromMillisecondsSinceEpoch(map['start_time'] * 1000),
endTime: DateTime.fromMillisecondsSinceEpoch(map['end_time'] * 1000),
duration: map['duration'],
category: map['category'],
projectId: map['project_id'],
deviceUnlockCount: map['device_unlock_count'] ?? 0,
createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at'] * 1000),
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updated_at'] * 1000),
);
}
// 格式化时长
String get formattedDuration {
final hours = duration ~/ 3600;
final minutes = (duration % 3600) ~/ 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
return '${minutes}m';
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class DailyStats {
final int? id;
final DateTime date;
final int totalTime; // 秒
final int workTime;
final int studyTime;
final int entertainmentTime;
final int socialTime;
final int toolTime;
final int? efficiencyScore;
final int? focusScore;
final int appSwitchCount;
final DateTime createdAt;
final DateTime updatedAt;
DailyStats({
this.id,
required this.date,
required this.totalTime,
this.workTime = 0,
this.studyTime = 0,
this.entertainmentTime = 0,
this.socialTime = 0,
this.toolTime = 0,
this.efficiencyScore,
this.focusScore,
this.appSwitchCount = 0,
required this.createdAt,
required this.updatedAt,
});
Map<String, int> get categoryTime => {
'work': workTime,
'study': studyTime,
'entertainment': entertainmentTime,
'social': socialTime,
'tool': toolTime,
};
// 格式化总时长
String get formattedTotalTime {
final hours = totalTime ~/ 3600;
final minutes = (totalTime % 3600) ~/ 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
return '${minutes}m';
}
// 获取效率评分颜色
Color get efficiencyColor {
final score = efficiencyScore ?? 0;
if (score >= 80) {
return const Color(0xFF10B981); // Green
} else if (score >= 50) {
return const Color(0xFFF59E0B); // Orange
} else {
return const Color(0xFFEF4444); // Red
}
}
}

54
lib/models/time_goal.dart Normal file
View File

@@ -0,0 +1,54 @@
class TimeGoal {
final int? id;
final String goalType; // 'daily_total' 或 'daily_category'
final String? category; // 如果是分类目标,指定分类
final int targetTime; // 目标时长(秒)
final bool isActive;
final DateTime createdAt;
final DateTime updatedAt;
TimeGoal({
this.id,
required this.goalType,
this.category,
required this.targetTime,
this.isActive = true,
required this.createdAt,
required this.updatedAt,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'goal_type': goalType,
'category': category,
'target_time': targetTime,
'is_active': isActive ? 1 : 0,
'created_at': createdAt.millisecondsSinceEpoch ~/ 1000,
'updated_at': updatedAt.millisecondsSinceEpoch ~/ 1000,
};
}
factory TimeGoal.fromMap(Map<String, dynamic> map) {
return TimeGoal(
id: map['id'] as int?,
goalType: map['goal_type'] as String,
category: map['category'] as String?,
targetTime: map['target_time'] as int,
isActive: (map['is_active'] as int) == 1,
createdAt: DateTime.fromMillisecondsSinceEpoch((map['created_at'] as int) * 1000),
updatedAt: DateTime.fromMillisecondsSinceEpoch((map['updated_at'] as int) * 1000),
);
}
// 格式化目标时长
String get formattedTargetTime {
final hours = targetTime ~/ 3600;
final minutes = (targetTime % 3600) ~/ 60;
if (hours > 0) {
return '${hours}小时${minutes}分钟';
}
return '${minutes}分钟';
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/background_sync_service.dart';
// BackgroundSyncService Provider
final backgroundSyncServiceProvider = Provider<BackgroundSyncService>((ref) {
final service = BackgroundSyncService();
// 当 Provider 被销毁时,停止服务
ref.onDispose(() {
service.stop();
});
return service;
});
// 后台同步状态 Provider
final backgroundSyncStatusProvider = StateProvider<bool>((ref) {
return false;
});

View File

@@ -0,0 +1,54 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/daily_stats.dart';
import '../models/app_usage.dart';
import '../services/statistics_service.dart';
// StatisticsService Provider
final statisticsServiceProvider = Provider<StatisticsService>((ref) {
return StatisticsService();
});
// 今日统计 Provider
final todayStatsProvider = FutureProvider<DailyStats>((ref) async {
final service = ref.read(statisticsServiceProvider);
return await service.getTodayStats();
});
// 本周统计 Provider
final weekStatsProvider = FutureProvider<List<DailyStats>>((ref) async {
final service = ref.read(statisticsServiceProvider);
return await service.getWeekStats();
});
// 本月统计 Provider
final monthStatsProvider = FutureProvider<List<DailyStats>>((ref) async {
final service = ref.read(statisticsServiceProvider);
return await service.getMonthStats();
});
// 今日统计列表 Provider用于日视图只返回今日数据
final todayStatsListProvider = FutureProvider<List<DailyStats>>((ref) async {
final service = ref.read(statisticsServiceProvider);
final todayStats = await service.getTodayStats();
return [todayStats];
});
// 今日 Top 应用 Provider
final todayTopAppsProvider = FutureProvider<List<AppUsage>>((ref) async {
final service = ref.read(statisticsServiceProvider);
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
return await service.getTopApps(
startTime: startOfDay,
endTime: endOfDay,
limit: 5,
);
});
// 刷新今日统计 Provider
final refreshTodayStatsProvider = FutureProvider.family<DailyStats, void>((ref, _) async {
final service = ref.read(statisticsServiceProvider);
return await service.refreshTodayStats();
});

View File

@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/time_tracking_service.dart';
// TimeTrackingService Provider
final timeTrackingServiceProvider = Provider<TimeTrackingService>((ref) {
return TimeTrackingService();
});
// 权限状态 Provider
final permissionStatusProvider = FutureProvider<bool>((ref) async {
final service = ref.read(timeTrackingServiceProvider);
return await service.hasPermission();
});
// 后台追踪状态 Provider
final backgroundTrackingStatusProvider = FutureProvider<bool>((ref) async {
final service = ref.read(timeTrackingServiceProvider);
return await service.isBackgroundTrackingActive();
});

View File

@@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class AboutScreen extends StatelessWidget {
const AboutScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('关于'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 应用图标和名称
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.timer,
color: Colors.white,
size: 48,
),
),
const SizedBox(height: 16),
Text(
'AutoTime Tracker',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'版本 1.0.0',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 32),
// 应用描述
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'关于应用',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
'AutoTime Tracker 是一款自动时间追踪与效率分析工具。'
'它可以帮助您自动追踪应用使用情况,分析时间分配,'
'并提供效率评分和个性化建议。',
style: theme.textTheme.bodyMedium,
),
],
),
),
),
const SizedBox(height: 16),
// 功能特点
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'核心功能',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
_buildFeatureItem(theme, Icons.auto_awesome, '自动追踪', '无需手动操作,自动记录应用使用时间'),
_buildFeatureItem(theme, Icons.category, '智能分类', '自动将应用分类为工作、学习、娱乐等'),
_buildFeatureItem(theme, Icons.insights, '数据分析', '提供详细的统计分析和效率评分'),
_buildFeatureItem(theme, Icons.flag, '目标设定', '设置时间目标,追踪完成情况'),
_buildFeatureItem(theme, Icons.file_download, '数据导出', '导出 CSV 和统计报告'),
],
),
),
),
const SizedBox(height: 16),
// 链接
Card(
child: Column(
children: [
_buildLinkItem(
context,
theme,
Icons.description,
'隐私政策',
'查看我们的隐私政策',
() {
// TODO: 打开隐私政策页面
_showComingSoon(context);
},
),
const Divider(height: 1),
_buildLinkItem(
context,
theme,
Icons.feedback,
'反馈建议',
'帮助我们改进应用',
() {
_launchEmail(context);
},
),
const Divider(height: 1),
_buildLinkItem(
context,
theme,
Icons.star,
'评价应用',
'在应用商店给我们评分',
() {
// TODO: 打开应用商店
_showComingSoon(context);
},
),
const Divider(height: 1),
_buildLinkItem(
context,
theme,
Icons.code,
'开源许可',
'MIT License',
() {
_showLicense(context, theme);
},
),
],
),
),
const SizedBox(height: 24),
// 版权信息
Text(
'© 2024 AutoTime Tracker',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
const SizedBox(height: 8),
Text(
'Made with ❤️ using Flutter',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
),
);
}
Widget _buildFeatureItem(ThemeData theme, IconData icon, String title, String description) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: AppTheme.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
],
),
);
}
Widget _buildLinkItem(
BuildContext context,
ThemeData theme,
IconData icon,
String title,
String subtitle,
VoidCallback onTap,
) {
return ListTile(
leading: Icon(icon, color: AppTheme.primaryColor),
title: Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
void _launchEmail(BuildContext context) {
// 显示反馈邮箱
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('反馈建议'),
content: const Text(
'欢迎通过以下方式联系我们:\n\n'
'邮箱support@autotime-tracker.com\n\n'
'我们非常重视您的反馈和建议!',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
],
),
);
}
void _showComingSoon(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('即将推出'),
content: const Text('此功能正在开发中,敬请期待!'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
],
),
);
}
void _showLicense(BuildContext context, ThemeData theme) {
showLicensePage(
context: context,
applicationName: 'AutoTime Tracker',
applicationVersion: '1.0.0',
applicationIcon: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.timer,
color: Colors.white,
size: 32,
),
),
);
}
}

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../theme/app_theme.dart';
class AppearanceSettingsScreen extends StatefulWidget {
const AppearanceSettingsScreen({super.key});
@override
State<AppearanceSettingsScreen> createState() => _AppearanceSettingsScreenState();
}
class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
ThemeMode _themeMode = ThemeMode.system;
double _fontSize = 1.0; // 1.0 = 正常0.8 = 小1.2 = 大
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
final themeModeIndex = prefs.getInt('theme_mode') ?? 0;
setState(() {
_themeMode = ThemeMode.values[themeModeIndex];
_fontSize = prefs.getDouble('font_size') ?? 1.0;
});
}
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme_mode', _themeMode.index);
await prefs.setDouble('font_size', _fontSize);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('外观设置'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 主题模式
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.palette, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'主题模式',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
RadioListTile<ThemeMode>(
title: const Text('跟随系统'),
subtitle: const Text('根据系统设置自动切换'),
value: ThemeMode.system,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
setState(() {
_themeMode = value;
});
_saveSettings();
// 通知应用更新主题
// 注意:这需要重启应用或使用 Provider 来管理主题
}
},
),
RadioListTile<ThemeMode>(
title: const Text('浅色模式'),
subtitle: const Text('始终使用浅色主题'),
value: ThemeMode.light,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
setState(() {
_themeMode = value;
});
_saveSettings();
}
},
),
RadioListTile<ThemeMode>(
title: const Text('深色模式'),
subtitle: const Text('始终使用深色主题'),
value: ThemeMode.dark,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
setState(() {
_themeMode = value;
});
_saveSettings();
}
},
),
],
),
),
),
const SizedBox(height: 16),
// 字体大小
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.text_fields, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'字体大小',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
Text(
'当前大小: ${_getFontSizeLabel(_fontSize)}',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
Slider(
value: _fontSize,
min: 0.8,
max: 1.2,
divisions: 4,
label: _getFontSizeLabel(_fontSize),
onChanged: (value) {
setState(() {
_fontSize = value;
});
_saveSettings();
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 12 * _fontSize,
),
),
Text(
'正常',
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14 * _fontSize,
),
),
Text(
'',
style: theme.textTheme.bodyLarge?.copyWith(
fontSize: 16 * _fontSize,
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// 图表样式(占位,未来功能)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.bar_chart, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'图表样式',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
ListTile(
title: const Text('图表颜色主题'),
subtitle: const Text('默认主题'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('此功能正在开发中'),
),
);
},
),
],
),
),
),
const SizedBox(height: 24),
// 说明
Card(
color: AppTheme.infoColor.withOpacity(0.1),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: AppTheme.infoColor),
const SizedBox(width: 12),
Expanded(
child: Text(
'主题模式更改需要重启应用才能生效。字体大小更改会立即生效。',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
),
],
),
),
),
],
),
),
);
}
String _getFontSizeLabel(double size) {
if (size <= 0.9) {
return '';
} else if (size <= 1.1) {
return '正常';
} else {
return '';
}
}
}

View File

@@ -0,0 +1,290 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../theme/app_theme.dart';
import '../services/category_service.dart';
import '../database/app_usage_dao.dart';
import '../widgets/empty_state_widget.dart';
import '../widgets/error_state_widget.dart';
class CategoryManagementScreen extends ConsumerStatefulWidget {
const CategoryManagementScreen({super.key});
@override
ConsumerState<CategoryManagementScreen> createState() => _CategoryManagementScreenState();
}
class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScreen> {
final CategoryService _categoryService = CategoryService();
final AppUsageDao _appUsageDao = AppUsageDao();
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
// 可用分类列表
final List<String> _availableCategories = ['work', 'study', 'entertainment', 'social', 'tool', 'other'];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<List<AppCategoryItem>> _loadAppCategories() async {
// 获取所有应用使用记录(最近使用的)
// 如果数据库为空返回空列表Web 平台会返回空列表)
final now = DateTime.now();
final startOfWeek = now.subtract(const Duration(days: 7));
final appUsages = await _appUsageDao.getAppUsages(
startTime: startOfWeek,
endTime: now,
);
// 如果没有数据,返回空列表
if (appUsages.isEmpty) {
return [];
}
// 获取自定义分类
final customCategories = await _categoryService.getAllCustomCategories();
// 去重并创建列表
final packageMap = <String, AppCategoryItem>{};
for (final usage in appUsages) {
if (!packageMap.containsKey(usage.packageName)) {
final currentCategory = customCategories[usage.packageName] ??
CategoryService.defaultCategories[usage.packageName] ??
'other';
packageMap[usage.packageName] = AppCategoryItem(
packageName: usage.packageName,
appName: usage.appName,
category: currentCategory,
isCustom: customCategories.containsKey(usage.packageName),
);
}
}
return packageMap.values.toList()
..sort((a, b) => a.appName.compareTo(b.appName));
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('应用分类'),
),
body: Column(
children: [
// 搜索框
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索应用...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
),
// 应用列表
Expanded(
child: FutureBuilder<List<AppCategoryItem>>(
future: _loadAppCategories(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorStateWidget.dataLoad(
message: _getErrorMessage(snapshot.error!),
onRetry: () {
setState(() {});
},
);
}
final items = snapshot.data ?? [];
final filteredItems = _searchQuery.isEmpty
? items
: items.where((item) {
return item.appName.toLowerCase().contains(_searchQuery.toLowerCase()) ||
item.packageName.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
if (filteredItems.isEmpty) {
if (_searchQuery.isEmpty) {
return EmptyStateWidget.noApps(
onAction: () {
setState(() {});
},
);
} else {
return EmptyStateWidget.noSearchResults(query: _searchQuery);
}
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filteredItems.length,
itemBuilder: (context, index) {
final item = filteredItems[index];
return _buildAppCategoryItem(context, theme, item);
},
);
},
),
),
],
),
);
}
Widget _buildAppCategoryItem(
BuildContext context,
ThemeData theme,
AppCategoryItem item,
) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(item.category).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.phone_android,
color: AppTheme.getCategoryColor(item.category),
),
),
title: Text(
item.appName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
item.packageName,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: PopupMenuButton<String>(
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(item.category).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
AppTheme.getCategoryName(item.category),
style: TextStyle(
color: AppTheme.getCategoryColor(item.category),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
],
),
onSelected: (String category) async {
await _categoryService.setCategory(item.packageName, category);
setState(() {});
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将 ${item.appName} 分类为 ${AppTheme.getCategoryName(category)}'),
duration: const Duration(seconds: 2),
),
);
}
},
itemBuilder: (BuildContext context) {
return _availableCategories.map((category) {
return PopupMenuItem<String>(
value: category,
child: Row(
children: [
if (item.category == category)
const Icon(Icons.check, size: 20, color: AppTheme.primaryColor)
else
const SizedBox(width: 20),
const SizedBox(width: 8),
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(category),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(AppTheme.getCategoryName(category)),
],
),
);
}).toList();
},
),
),
);
}
String _getErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('network') || errorString.contains('网络')) {
return '网络连接失败,请检查网络';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败';
}
return '加载失败,请稍后重试';
}
}
class AppCategoryItem {
final String packageName;
final String appName;
final String category;
final bool isCustom;
AppCategoryItem({
required this.packageName,
required this.appName,
required this.category,
required this.isCustom,
});
}

View File

@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../theme/app_theme.dart';
import '../database/app_usage_dao.dart';
import '../database/daily_stats_dao.dart';
class DataPrivacyScreen extends StatefulWidget {
const DataPrivacyScreen({super.key});
@override
State<DataPrivacyScreen> createState() => _DataPrivacyScreenState();
}
class _DataPrivacyScreenState extends State<DataPrivacyScreen> {
final AppUsageDao _appUsageDao = AppUsageDao();
final DailyStatsDao _dailyStatsDao = DailyStatsDao();
bool _isDeleting = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('数据与隐私'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 隐私说明
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.security, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'隐私保护',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
_buildPrivacyItem(
theme,
Icons.storage,
'本地存储',
'所有数据仅存储在您的设备本地,不会上传到任何服务器。',
),
_buildPrivacyItem(
theme,
Icons.lock,
'数据加密',
'敏感数据在存储时进行加密处理,确保数据安全。',
),
_buildPrivacyItem(
theme,
Icons.visibility_off,
'隐私保护',
'我们不会收集您的个人信息,也不会追踪您的具体操作内容。',
),
_buildPrivacyItem(
theme,
Icons.delete_forever,
'完全控制',
'您可以随时删除所有数据,完全掌控您的隐私。',
),
],
),
),
),
const SizedBox(height: 16),
// 数据管理
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'数据管理',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
_buildDataAction(
context,
theme,
Icons.delete_outline,
'删除旧数据',
'删除 30 天前的数据',
Colors.orange,
() => _showDeleteOldDataDialog(context),
),
const SizedBox(height: 12),
_buildDataAction(
context,
theme,
Icons.delete_forever,
'清空所有数据',
'删除所有使用记录和统计数据',
Colors.red,
() => _showDeleteAllDataDialog(context),
),
],
),
),
),
const SizedBox(height: 16),
// 数据使用说明
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'数据使用说明',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Text(
'• 应用使用数据仅用于统计和分析\n'
'• 数据不会离开您的设备\n'
'• 不会与第三方分享任何数据\n'
'• 不会用于广告或营销目的\n'
'• 您可以随时导出或删除数据',
style: theme.textTheme.bodyMedium,
),
],
),
),
),
],
),
),
);
}
Widget _buildPrivacyItem(ThemeData theme, IconData icon, String title, String description) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: AppTheme.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
],
),
);
}
Widget _buildDataAction(
BuildContext context,
ThemeData theme,
IconData icon,
String title,
String subtitle,
Color color,
VoidCallback onTap,
) {
return InkWell(
onTap: _isDeleting ? null : onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: color.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(icon, color: color),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
color: color,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
if (_isDeleting)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
const Icon(Icons.chevron_right),
],
),
),
);
}
void _showDeleteOldDataDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除旧数据'),
content: const Text('确定要删除 30 天前的所有数据吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
await _deleteOldData(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('删除'),
),
],
),
);
}
void _showDeleteAllDataDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('清空所有数据'),
content: const Text(
'确定要删除所有使用记录和统计数据吗?\n\n'
'此操作将:\n'
'• 删除所有应用使用记录\n'
'• 删除所有统计数据\n'
'• 删除所有分类设置\n'
'• 删除所有目标设置\n\n'
'此操作不可恢复!',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
await _deleteAllData(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('确认删除'),
),
],
),
);
}
Future<void> _deleteOldData(BuildContext context) async {
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Web 平台不支持数据删除功能'),
backgroundColor: AppTheme.warningColor,
),
);
return;
}
setState(() {
_isDeleting = true;
});
try {
final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30));
await _appUsageDao.deleteBeforeDate(thirtyDaysAgo);
await _dailyStatsDao.deleteBeforeDate(thirtyDaysAgo);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除 30 天前的数据'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('删除失败: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} finally {
if (mounted) {
setState(() {
_isDeleting = false;
});
}
}
}
Future<void> _deleteAllData(BuildContext context) async {
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Web 平台不支持数据删除功能'),
backgroundColor: AppTheme.warningColor,
),
);
return;
}
setState(() {
_isDeleting = true;
});
try {
// 删除所有应用使用记录
final allUsages = await _appUsageDao.getAppUsages(
startTime: DateTime(2000),
endTime: DateTime.now(),
);
for (final usage in allUsages) {
if (usage.id != null) {
await _appUsageDao.deleteAppUsage(usage.id!);
}
}
// 删除所有统计数据
final allStats = await _dailyStatsDao.getStatsRange(
startDate: DateTime(2000),
endDate: DateTime.now(),
);
for (final stat in allStats) {
if (stat.id != null) {
await _dailyStatsDao.deleteDailyStats(stat.id!);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已清空所有数据'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('删除失败: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} finally {
if (mounted) {
setState(() {
_isDeleting = false;
});
}
}
}
}

View File

@@ -0,0 +1,403 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../theme/app_theme.dart';
import '../services/export_service.dart';
// Web 平台需要的导入
import 'dart:html' as html show Blob, Url, AnchorElement;
class ExportDataScreen extends StatefulWidget {
const ExportDataScreen({super.key});
@override
State<ExportDataScreen> createState() => _ExportDataScreenState();
}
class _ExportDataScreenState extends State<ExportDataScreen> {
final ExportService _exportService = ExportService();
DateTime _startDate = DateTime.now().subtract(const Duration(days: 7));
DateTime _endDate = DateTime.now();
bool _isExporting = false;
Future<void> _exportCSV() async {
setState(() {
_isExporting = true;
});
try {
final csvData = await _exportService.exportToCSV(
startDate: _startDate,
endDate: _endDate,
);
if (csvData.isEmpty || csvData.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('所选日期范围内没有数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([csvData], 'text/csv');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_export_${DateTime.now().millisecondsSinceEpoch}.csv')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: csvData));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '数据已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _exportReport() async {
setState(() {
_isExporting = true;
});
try {
final report = await _exportService.exportStatsReport(
startDate: _startDate,
endDate: _endDate,
);
if (report.isEmpty || report.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('所选日期范围内没有统计数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([report], 'text/plain');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_report_${DateTime.now().millisecondsSinceEpoch}.txt')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: report));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '报告已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _exportTodayReport() async {
setState(() {
_isExporting = true;
});
try {
final report = await _exportService.exportTodayReport();
if (report.isEmpty || report.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('今日暂无数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([report], 'text/plain');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_today_${DateTime.now().millisecondsSinceEpoch}.txt')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: report));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '今日报告已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportTodayReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _selectDateRange() async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(start: _startDate, end: _endDate),
);
if (picked != null) {
setState(() {
_startDate = picked.start;
_endDate = picked.end;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('数据导出'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 日期范围选择
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择日期范围',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _selectDateRange,
icon: const Icon(Icons.calendar_today),
label: Text(
'${_startDate.toString().split(' ')[0]}${_endDate.toString().split(' ')[0]}',
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// 导出选项
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'导出选项',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// CSV 导出
_buildExportOption(
theme,
icon: Icons.table_chart,
title: '导出 CSV 数据',
subtitle: '导出原始应用使用数据CSV 格式)',
onTap: _exportCSV,
),
const SizedBox(height: 12),
// 统计报告
_buildExportOption(
theme,
icon: Icons.description,
title: '导出统计报告',
subtitle: '导出时间范围内的统计报告(文本格式)',
onTap: _exportReport,
),
const SizedBox(height: 12),
// 今日报告
_buildExportOption(
theme,
icon: Icons.today,
title: '导出今日报告',
subtitle: '导出今日的详细统计报告',
onTap: _exportTodayReport,
),
],
),
),
),
if (_isExporting)
const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
);
}
Widget _buildExportOption(
ThemeData theme, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return InkWell(
onTap: _isExporting ? null : onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: theme.colorScheme.onSurface.withOpacity(0.1),
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(icon, color: AppTheme.primaryColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
);
}
String _getExportErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败,请稍后重试';
} else if (errorString.contains('file') || errorString.contains('文件')) {
return '文件操作失败,请检查存储权限';
}
return '导出失败,请稍后重试';
}
}

View File

@@ -0,0 +1,494 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../theme/app_theme.dart';
import '../models/time_goal.dart';
import '../database/time_goal_dao.dart';
class GoalSettingScreen extends ConsumerStatefulWidget {
const GoalSettingScreen({super.key});
@override
ConsumerState<GoalSettingScreen> createState() => _GoalSettingScreenState();
}
class _GoalSettingScreenState extends ConsumerState<GoalSettingScreen> {
final TimeGoalDao _goalDao = TimeGoalDao();
TimeGoal? _dailyTotalGoal;
final Map<String, TimeGoal?> _categoryGoals = {};
@override
void initState() {
super.initState();
_loadGoals();
}
Future<void> _loadGoals() async {
// 获取所有目标(包括非激活的),以便正确显示开关状态
final allGoals = await _goalDao.getAllGoals();
setState(() {
// 查找每日总时长目标
final dailyTotal = allGoals.firstWhere(
(g) => g.goalType == 'daily_total',
orElse: () => TimeGoal(
goalType: 'daily_total',
targetTime: 28800, // 默认 8 小时
isActive: _dailyTotalGoal?.isActive ?? true, // 保持当前状态
createdAt: _dailyTotalGoal?.createdAt ?? DateTime.now(),
updatedAt: DateTime.now(),
),
);
// 如果找到的目标有 ID使用它否则保持当前 ID如果有
_dailyTotalGoal = TimeGoal(
id: dailyTotal.id ?? _dailyTotalGoal?.id,
goalType: dailyTotal.goalType,
targetTime: dailyTotal.targetTime,
isActive: dailyTotal.isActive,
createdAt: dailyTotal.createdAt,
updatedAt: dailyTotal.updatedAt,
);
final categories = ['work', 'study', 'entertainment', 'social', 'tool'];
for (final category in categories) {
_categoryGoals[category] = allGoals.firstWhere(
(g) => g.goalType == 'daily_category' && g.category == category,
orElse: () => TimeGoal(
goalType: 'daily_category',
category: category,
targetTime: 0,
isActive: false,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
}
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('时间目标'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 每日总时长目标
_buildDailyTotalGoalCard(theme),
const SizedBox(height: 16),
// 分类时间限制
_buildCategoryGoalsCard(theme),
],
),
),
);
}
Widget _buildDailyTotalGoalCard(ThemeData theme) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.timer, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'每日总时长目标',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
if (_dailyTotalGoal != null) ...[
Row(
children: [
Expanded(
child: Text(
_dailyTotalGoal!.formattedTargetTime,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
),
Switch(
value: _dailyTotalGoal!.isActive,
onChanged: (value) async {
// 先更新本地状态,立即显示变化
setState(() {
_dailyTotalGoal = TimeGoal(
id: _dailyTotalGoal!.id,
goalType: _dailyTotalGoal!.goalType,
targetTime: _dailyTotalGoal!.targetTime,
isActive: value,
createdAt: _dailyTotalGoal!.createdAt,
updatedAt: DateTime.now(),
);
});
// 然后保存到数据库
await _goalDao.upsertTimeGoal(_dailyTotalGoal!);
// 重新加载以确保数据同步
await _loadGoals();
},
),
],
),
const SizedBox(height: 16),
_buildTimePicker(
theme,
'设置目标时长',
_dailyTotalGoal!.targetTime,
(hours, minutes) async {
final targetTime = hours * 3600 + minutes * 60;
final updatedGoal = TimeGoal(
id: _dailyTotalGoal!.id,
goalType: 'daily_total',
targetTime: targetTime,
isActive: _dailyTotalGoal!.isActive,
createdAt: _dailyTotalGoal!.createdAt,
updatedAt: DateTime.now(),
);
await _goalDao.upsertTimeGoal(updatedGoal);
await _loadGoals();
},
),
],
],
),
),
);
}
Widget _buildCategoryGoalsCard(ThemeData theme) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.category, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'分类时间限制',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
...['work', 'study', 'entertainment', 'social', 'tool'].map((category) {
final goal = _categoryGoals[category];
return _buildCategoryGoalItem(theme, category, goal);
}),
],
),
),
);
}
Widget _buildCategoryGoalItem(
ThemeData theme,
String category,
TimeGoal? goal,
) {
final isActive = goal?.isActive ?? false;
final targetTime = goal?.targetTime ?? 0;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(category),
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
AppTheme.getCategoryName(category),
style: theme.textTheme.bodyLarge,
),
),
Text(
targetTime > 0 ? _formatTime(targetTime) : '未设置',
style: theme.textTheme.bodyMedium?.copyWith(
color: isActive ? AppTheme.primaryColor : theme.colorScheme.onSurface.withOpacity(0.5),
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
),
),
const SizedBox(width: 12),
Switch(
value: isActive,
onChanged: (value) async {
if (value && targetTime == 0) {
// 如果启用但未设置时间,先设置默认值
final defaultGoal = TimeGoal(
id: goal?.id,
goalType: 'daily_category',
category: category,
targetTime: 7200, // 默认 2 小时
isActive: true,
createdAt: goal?.createdAt ?? DateTime.now(),
updatedAt: DateTime.now(),
);
await _goalDao.upsertTimeGoal(defaultGoal);
} else {
final updatedGoal = TimeGoal(
id: goal?.id,
goalType: 'daily_category',
category: category,
targetTime: targetTime,
isActive: value,
createdAt: goal?.createdAt ?? DateTime.now(),
updatedAt: DateTime.now(),
);
await _goalDao.upsertTimeGoal(updatedGoal);
}
await _loadGoals();
},
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
_showCategoryGoalDialog(theme, category, goal);
},
),
],
),
);
}
void _showCategoryGoalDialog(ThemeData theme, String category, TimeGoal? goal) {
final currentTime = goal?.targetTime ?? 0;
final hours = currentTime ~/ 3600;
final minutes = (currentTime % 3600) ~/ 60;
showDialog(
context: context,
builder: (context) => _TimePickerDialog(
title: '设置 ${AppTheme.getCategoryName(category)} 时间限制',
initialHours: hours,
initialMinutes: minutes,
onSave: (hours, minutes) async {
final targetTime = hours * 3600 + minutes * 60;
final updatedGoal = TimeGoal(
id: goal?.id,
goalType: 'daily_category',
category: category,
targetTime: targetTime,
isActive: true,
createdAt: goal?.createdAt ?? DateTime.now(),
updatedAt: DateTime.now(),
);
await _goalDao.upsertTimeGoal(updatedGoal);
await _loadGoals();
},
),
);
}
Widget _buildTimePicker(
ThemeData theme,
String title,
int currentTime,
Future<void> Function(int hours, int minutes) onSave,
) {
final hours = currentTime ~/ 3600;
final minutes = (currentTime % 3600) ~/ 60;
return ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (context) => _TimePickerDialog(
title: title,
initialHours: hours,
initialMinutes: minutes,
onSave: onSave,
),
);
},
icon: const Icon(Icons.access_time),
label: Text('${hours}小时 ${minutes}分钟'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
String _formatTime(int seconds) {
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
return '${minutes}m';
}
}
class _TimePickerDialog extends StatefulWidget {
final String title;
final int initialHours;
final int initialMinutes;
final Future<void> Function(int hours, int minutes) onSave;
const _TimePickerDialog({
required this.title,
required this.initialHours,
required this.initialMinutes,
required this.onSave,
});
@override
State<_TimePickerDialog> createState() => _TimePickerDialogState();
}
class _TimePickerDialogState extends State<_TimePickerDialog> {
late int _hours;
late int _minutes;
@override
void initState() {
super.initState();
_hours = widget.initialHours;
_minutes = widget.initialMinutes;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Text(widget.title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 小时选择
Column(
children: [
Text('小时', style: theme.textTheme.bodySmall),
const SizedBox(height: 8),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
setState(() {
if (_hours > 0) _hours--;
});
},
),
SizedBox(
width: 60,
child: Text(
'$_hours',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium,
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
if (_hours < 24) _hours++;
});
},
),
],
),
],
),
const SizedBox(width: 24),
// 分钟选择
Column(
children: [
Text('分钟', style: theme.textTheme.bodySmall),
const SizedBox(height: 8),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
setState(() {
if (_minutes > 0) {
_minutes -= 15;
if (_minutes < 0) _minutes = 0;
}
});
},
),
SizedBox(
width: 60,
child: Text(
'$_minutes',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium,
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
_minutes += 15;
if (_minutes >= 60) {
_hours++;
_minutes = 0;
}
if (_hours >= 24) {
_hours = 23;
_minutes = 59;
}
});
},
),
],
),
],
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
await widget.onSave(_hours, _minutes);
if (mounted) {
Navigator.of(context).pop();
}
},
child: const Text('保存'),
),
],
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'today_screen.dart';
import 'stats_screen.dart';
import 'settings_screen.dart';
import '../providers/background_sync_provider.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const TodayScreen(),
const StatsScreen(),
const SettingsScreen(),
];
@override
void initState() {
super.initState();
// Web 平台不启动后台同步服务
if (!kIsWeb) {
// 启动后台同步服务
WidgetsBinding.instance.addPostFrameCallback((_) {
final syncService = ref.read(backgroundSyncServiceProvider);
syncService.start();
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _screens,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() {
_currentIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.today_outlined),
selectedIcon: Icon(Icons.today),
label: 'Today',
),
NavigationDestination(
icon: Icon(Icons.bar_chart_outlined),
selectedIcon: Icon(Icons.bar_chart),
label: 'Stats',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../theme/app_theme.dart';
import '../widgets/custom_time_picker_dialog.dart';
class NotificationSettingsScreen extends StatefulWidget {
const NotificationSettingsScreen({super.key});
@override
State<NotificationSettingsScreen> createState() => _NotificationSettingsScreenState();
}
class _NotificationSettingsScreenState extends State<NotificationSettingsScreen> {
bool _goalReminderEnabled = true;
bool _dailyReportEnabled = true;
bool _weeklyReportEnabled = false;
TimeOfDay _goalReminderTime = const TimeOfDay(hour: 20, minute: 0);
TimeOfDay _dailyReportTime = const TimeOfDay(hour: 22, minute: 0);
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_goalReminderEnabled = prefs.getBool('goal_reminder_enabled') ?? true;
_dailyReportEnabled = prefs.getBool('daily_report_enabled') ?? true;
_weeklyReportEnabled = prefs.getBool('weekly_report_enabled') ?? false;
final goalReminderHour = prefs.getInt('goal_reminder_hour') ?? 20;
final goalReminderMinute = prefs.getInt('goal_reminder_minute') ?? 0;
_goalReminderTime = TimeOfDay(hour: goalReminderHour, minute: goalReminderMinute);
final dailyReportHour = prefs.getInt('daily_report_hour') ?? 22;
final dailyReportMinute = prefs.getInt('daily_report_minute') ?? 0;
_dailyReportTime = TimeOfDay(hour: dailyReportHour, minute: dailyReportMinute);
});
}
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('goal_reminder_enabled', _goalReminderEnabled);
await prefs.setBool('daily_report_enabled', _dailyReportEnabled);
await prefs.setBool('weekly_report_enabled', _weeklyReportEnabled);
await prefs.setInt('goal_reminder_hour', _goalReminderTime.hour);
await prefs.setInt('goal_reminder_minute', _goalReminderTime.minute);
await prefs.setInt('daily_report_hour', _dailyReportTime.hour);
await prefs.setInt('daily_report_minute', _dailyReportTime.minute);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('通知设置'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 目标提醒
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.flag, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'目标提醒',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('启用目标提醒'),
subtitle: const Text('当接近或超过时间目标时提醒'),
value: _goalReminderEnabled,
onChanged: (value) {
setState(() {
_goalReminderEnabled = value;
});
_saveSettings();
},
),
if (_goalReminderEnabled) ...[
const SizedBox(height: 8),
ListTile(
title: const Text('提醒时间'),
subtitle: Text(
'${_goalReminderTime.hour.toString().padLeft(2, '0')}:${_goalReminderTime.minute.toString().padLeft(2, '0')}',
),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final TimeOfDay? picked = await CustomTimePickerDialog.show(
context: context,
title: '选择提醒时间',
initialTime: _goalReminderTime,
);
if (picked != null) {
setState(() {
_goalReminderTime = picked;
});
await _saveSettings();
}
},
),
],
],
),
),
),
const SizedBox(height: 16),
// 每日报告
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.today, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'每日报告',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('启用每日报告'),
subtitle: const Text('每天发送使用时间摘要'),
value: _dailyReportEnabled,
onChanged: (value) {
setState(() {
_dailyReportEnabled = value;
});
_saveSettings();
},
),
if (_dailyReportEnabled) ...[
const SizedBox(height: 8),
ListTile(
title: const Text('报告时间'),
subtitle: Text(
'${_dailyReportTime.hour.toString().padLeft(2, '0')}:${_dailyReportTime.minute.toString().padLeft(2, '0')}',
),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final TimeOfDay? picked = await CustomTimePickerDialog.show(
context: context,
title: '选择报告时间',
initialTime: _dailyReportTime,
);
if (picked != null) {
setState(() {
_dailyReportTime = picked;
});
await _saveSettings();
}
},
),
],
],
),
),
),
const SizedBox(height: 16),
// 每周报告
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.calendar_view_week, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'每周报告',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('启用每周报告'),
subtitle: const Text('每周一发送周报摘要'),
value: _weeklyReportEnabled,
onChanged: (value) {
setState(() {
_weeklyReportEnabled = value;
});
_saveSettings();
},
),
],
),
),
),
const SizedBox(height: 24),
// 说明
Card(
color: AppTheme.infoColor.withOpacity(0.1),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: AppTheme.infoColor),
const SizedBox(width: 12),
Expanded(
child: Text(
'通知功能需要系统通知权限。请在系统设置中授予通知权限。',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:flutter/foundation.dart' show TargetPlatform;
import '../providers/time_tracking_provider.dart';
import '../widgets/error_state_widget.dart';
class PermissionScreen extends ConsumerWidget {
const PermissionScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final permissionStatus = ref.watch(permissionStatusProvider);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('权限设置'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 图标
Icon(
Icons.security,
size: 80,
color: theme.colorScheme.primary,
),
const SizedBox(height: 24),
// 标题
Text(
'我们需要访问您的应用使用数据',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// 说明
Text(
_getPermissionDescription(),
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// 权限状态
permissionStatus.when(
data: (hasPermission) {
if (hasPermission) {
return _buildPermissionGranted(context, theme);
} else {
return _buildPermissionRequest(context, ref, theme);
}
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorStateWidget.generic(
message: '检查权限时出错,请重试',
onRetry: () {
ref.invalidate(permissionStatusProvider);
},
),
),
const SizedBox(height: 32),
// 隐私说明
_buildPrivacyInfo(theme),
],
),
),
);
}
String _getPermissionDescription() {
if (kIsWeb) {
return 'Web 平台暂不支持时间追踪功能。\n\n'
'请使用 iOS 或 Android 应用。';
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return '这样我们才能自动追踪您的应用使用情况,无需手动操作。\n\n'
'• 完全自动化,无需手动操作\n'
'• 数据仅存储在本地\n'
'• 不会上传到服务器';
} else {
return '这样我们才能自动追踪您的应用使用情况,无需手动操作。\n\n'
'• 完全自动化,无需手动操作\n'
'• 数据仅存储在本地\n'
'• 不会上传到服务器';
}
}
Widget _buildPermissionRequest(BuildContext context, WidgetRef ref, ThemeData theme) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: theme.colorScheme.error,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'权限未授予',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.error,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () async {
final service = ref.read(timeTrackingServiceProvider);
final granted = await service.requestPermission();
if (granted) {
ref.invalidate(permissionStatusProvider);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('权限已授予')),
);
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('权限授予失败,请前往设置中手动开启')),
);
}
}
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'去设置',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('稍后再说'),
),
],
);
}
Widget _buildPermissionGranted(BuildContext context, ThemeData theme) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.check_circle,
color: theme.colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'权限已授予',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildPrivacyInfo(ThemeData theme) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'隐私说明',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'• 所有数据仅存储在您的设备本地\n'
'• 我们不会收集或上传任何数据到服务器\n'
'• 您可以随时删除所有数据\n'
'• 应用使用数据仅用于统计和分析',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import 'category_management_screen.dart';
import 'goal_setting_screen.dart';
import 'export_data_screen.dart';
import 'data_privacy_screen.dart';
import 'about_screen.dart';
import 'notification_settings_screen.dart';
import 'appearance_settings_screen.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSettingsSection(
title: '应用分类',
icon: Icons.category,
subtitle: '管理应用分类规则',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CategoryManagementScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 8),
_buildSettingsSection(
title: '时间目标',
icon: Icons.flag,
subtitle: '设置每日时间目标',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const GoalSettingScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 8),
_buildSettingsSection(
title: '数据导出',
icon: Icons.file_download,
subtitle: '导出 CSV 数据、统计报告',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ExportDataScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 8),
_buildSettingsSection(
title: '数据与隐私',
icon: Icons.security,
subtitle: '数据管理、删除',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DataPrivacyScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 8),
_buildSettingsSection(
title: '通知设置',
icon: Icons.notifications,
subtitle: '目标提醒、每日报告',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationSettingsScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 8),
_buildSettingsSection(
title: '外观设置',
icon: Icons.palette,
subtitle: '主题、字体大小',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AppearanceSettingsScreen(),
),
);
},
theme: theme,
),
const SizedBox(height: 24),
_buildUpgradeCard(theme),
const SizedBox(height: 24),
_buildSettingsSection(
title: '关于',
icon: Icons.info,
subtitle: '版本信息、帮助、反馈',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AboutScreen(),
),
);
},
theme: theme,
),
],
),
);
}
Widget _buildSettingsSection({
required String title,
required IconData icon,
required String subtitle,
required VoidCallback onTap,
required ThemeData theme,
}) {
return Card(
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: AppTheme.primaryColor,
),
),
title: Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
Widget _buildUpgradeCard(ThemeData theme) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.secondaryColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.diamond,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Text(
'Upgrade to Pro',
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
Text(
'解锁高级功能',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 8),
...[
'无限历史数据',
'高级统计分析',
'效率评分与分析',
'个性化建议',
'数据导出',
].map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
const Icon(
Icons.check_circle,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
Text(
feature,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
],
),
)),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// 打开订阅页面
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'立即升级',
style: TextStyle(
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,564 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../theme/app_theme.dart';
import '../providers/statistics_provider.dart';
import '../models/daily_stats.dart';
import '../widgets/empty_state_widget.dart';
import '../widgets/error_state_widget.dart';
class StatsScreen extends ConsumerStatefulWidget {
const StatsScreen({super.key});
@override
ConsumerState<StatsScreen> createState() => _StatsScreenState();
}
class _StatsScreenState extends ConsumerState<StatsScreen> {
String _selectedPeriod = ''; // 日、周、月
// 根据时间段获取图表数据
List<Map<String, dynamic>> _getChartData(List<DailyStats>? stats, String period) {
if (stats != null && stats.isNotEmpty) {
return stats.map((stat) {
return {
'date': stat.date,
'total': stat.totalTime,
'work': stat.workTime,
'study': stat.studyTime,
'entertainment': stat.entertainmentTime,
};
}).toList();
}
// 如果没有数据,使用默认测试数据
final now = DateTime.now();
if (period == '') {
// 日视图显示今日数据24小时每小时一个点
return List.generate(24, (index) {
final hour = now.subtract(Duration(hours: 23 - index));
return {
'date': hour,
'total': 1800 + (index % 3) * 300, // 模拟每小时30-60分钟
'work': 1200 + (index % 3) * 200,
'study': 300 + (index % 3) * 50,
'entertainment': 300 + (index % 3) * 50,
};
});
} else if (period == '') {
// 周视图显示7天数据
return [
{'date': now.subtract(const Duration(days: 6)), 'total': 21600, 'work': 14400, 'study': 3600, 'entertainment': 3600},
{'date': now.subtract(const Duration(days: 5)), 'total': 25200, 'work': 18000, 'study': 3600, 'entertainment': 3600},
{'date': now.subtract(const Duration(days: 4)), 'total': 23400, 'work': 16200, 'study': 3600, 'entertainment': 3600},
{'date': now.subtract(const Duration(days: 3)), 'total': 19800, 'work': 12600, 'study': 3600, 'entertainment': 3600},
{'date': now.subtract(const Duration(days: 2)), 'total': 27000, 'work': 19800, 'study': 3600, 'entertainment': 3600},
{'date': now.subtract(const Duration(days: 1)), 'total': 22500, 'work': 15300, 'study': 3600, 'entertainment': 3600},
{'date': now, 'total': 23040, 'work': 14400, 'study': 3600, 'entertainment': 3600},
];
} else {
// 月视图显示30天数据简化版每天一个点
return List.generate(30, (index) {
final date = now.subtract(Duration(days: 29 - index));
return {
'date': date,
'total': 18000 + (index % 7) * 2000, // 模拟每天5-7小时
'work': 12000 + (index % 7) * 1500,
'study': 3000 + (index % 7) * 300,
'entertainment': 3000 + (index % 7) * 200,
};
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// 根据选中的时间段选择不同的 Provider
final statsAsync = _selectedPeriod == ''
? ref.watch(todayStatsListProvider)
: _selectedPeriod == ''
? ref.watch(weekStatsProvider)
: ref.watch(monthStatsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Statistics'),
actions: [
IconButton(
icon: const Icon(Icons.file_download),
onPressed: () {
// 导出数据
},
),
],
),
body: statsAsync.when(
data: (stats) {
final chartData = _getChartData(stats, _selectedPeriod);
// 检查是否为空数据
final isEmpty = chartData.isEmpty || chartData.every((data) => (data['total'] as int) == 0);
if (isEmpty) {
return EmptyStateWidget.noData(
title: '暂无统计数据',
subtitle: '使用应用一段时间后,统计数据将显示在这里',
actionLabel: '刷新',
onAction: () {
if (_selectedPeriod == '') {
ref.invalidate(todayStatsListProvider);
} else if (_selectedPeriod == '') {
ref.invalidate(weekStatsProvider);
} else {
ref.invalidate(monthStatsProvider);
}
},
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 时间选择器
_buildPeriodSelector(theme),
const SizedBox(height: 24),
// 时间趋势图
_buildTrendChart(theme, chartData, _selectedPeriod),
const SizedBox(height: 24),
// 分类对比图
_buildCategoryChart(theme, chartData, _selectedPeriod),
const SizedBox(height: 24),
// 应用使用详情
_buildAppDetails(theme),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorStateWidget.dataLoad(
message: _getErrorMessage(error),
onRetry: () {
if (_selectedPeriod == '') {
ref.invalidate(todayStatsListProvider);
} else if (_selectedPeriod == '') {
ref.invalidate(weekStatsProvider);
} else {
ref.invalidate(monthStatsProvider);
}
},
),
),
);
}
Widget _buildPeriodSelector(ThemeData theme) {
return Row(
children: [
Expanded(
child: SegmentedButton<String>(
segments: const [
ButtonSegment(value: '', label: Text('')),
ButtonSegment(value: '', label: Text('')),
ButtonSegment(value: '', label: Text('')),
],
selected: {_selectedPeriod},
onSelectionChanged: (Set<String> newSelection) {
final newPeriod = newSelection.first;
setState(() {
_selectedPeriod = newPeriod;
});
// 切换时间段时,刷新对应的 Provider
if (newPeriod == '') {
ref.invalidate(todayStatsListProvider);
} else if (newPeriod == '') {
ref.invalidate(weekStatsProvider);
} else {
ref.invalidate(monthStatsProvider);
}
},
),
),
const SizedBox(width: 12),
IconButton(
icon: const Icon(Icons.file_download),
onPressed: () {
// 导出功能
},
),
],
);
}
Widget _buildTrendChart(ThemeData theme, List<Map<String, dynamic>> chartData, String period) {
return Container(
height: 250,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
period == '' ? '每小时总时长趋势' : period == '' ? '每日总时长趋势' : '每日总时长趋势(月)',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Expanded(
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 2,
getDrawingHorizontalLine: (value) {
return FlLine(
color: theme.colorScheme.onSurface.withOpacity(0.1),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 3600).toStringAsFixed(1)}h',
style: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value.toInt() >= 0 && value.toInt() < chartData.length) {
final date = chartData[value.toInt()]['date'] as DateTime;
String label;
if (period == '') {
label = DateFormat('HH:mm', 'zh_CN').format(date);
} else if (period == '') {
label = DateFormat('E', 'zh_CN').format(date);
} else {
label = DateFormat('M/d', 'zh_CN').format(date);
}
return Text(
label,
style: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
);
}
return const Text('');
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: chartData.asMap().entries.map((entry) {
return FlSpot(
entry.key.toDouble(),
(entry.value['total'] as int) / 3600.0,
);
}).toList(),
isCurved: true,
color: AppTheme.primaryColor,
barWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
],
),
),
),
],
),
);
}
Widget _buildCategoryChart(ThemeData theme, List<Map<String, dynamic>> chartData, String period) {
return Container(
height: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
period == '' ? '每小时分类时间分布' : period == '' ? '每日分类时间分布' : '每日分类时间分布(月)',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Expanded(
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 8,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()}h',
style: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value.toInt() >= 0 && value.toInt() < chartData.length) {
final date = chartData[value.toInt()]['date'] as DateTime;
String label;
if (period == '') {
label = DateFormat('HH:mm', 'zh_CN').format(date);
} else if (period == '') {
label = DateFormat('M/d', 'zh_CN').format(date);
} else {
label = DateFormat('M/d', 'zh_CN').format(date);
}
return Text(
label,
style: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
);
}
return const Text('');
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) {
return FlLine(
color: theme.colorScheme.onSurface.withOpacity(0.1),
strokeWidth: 1,
);
},
),
borderData: FlBorderData(show: false),
barGroups: chartData.asMap().entries.map((entry) {
final data = entry.value;
return BarChartGroupData(
x: entry.key,
barRods: [
BarChartRodData(
toY: (data['work'] as int) / 3600.0,
color: AppTheme.workColor,
width: 12,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
BarChartRodData(
toY: (data['study'] as int) / 3600.0,
color: AppTheme.studyColor,
width: 12,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
BarChartRodData(
toY: (data['entertainment'] as int) / 3600.0,
color: AppTheme.entertainmentColor,
width: 12,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
],
);
}).toList(),
),
),
),
],
),
);
}
Widget _buildAppDetails(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'应用使用详情',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
_buildAppDetailItem('Chrome', '今日: 2h 15m', '本周: 12h 30m', '工作', theme),
_buildAppDetailItem('VS Code', '今日: 1h 30m', '本周: 8h 45m', '工作', theme),
_buildAppDetailItem('Slack', '今日: 1h', '本周: 6h', '工作', theme),
],
);
}
Widget _buildAppDetailItem(
String appName,
String todayTime,
String weekTime,
String category,
ThemeData theme,
) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(category).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.phone_android,
color: AppTheme.getCategoryColor(category),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'分类: $category',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 4),
Text(
todayTime,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
),
],
),
Container(
width: 1,
height: 40,
color: theme.colorScheme.onSurface.withOpacity(0.1),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'本周',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 4),
Text(
weekTime,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
),
],
),
],
),
],
),
);
}
String _getErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('network') || errorString.contains('网络')) {
return '网络连接失败,请检查网络';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败';
}
return '加载失败,请稍后重试';
}
}

View File

@@ -0,0 +1,407 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fl_chart/fl_chart.dart';
import '../theme/app_theme.dart';
import '../models/daily_stats.dart';
import '../models/app_usage.dart';
import '../providers/statistics_provider.dart';
import '../widgets/empty_state_widget.dart';
import '../widgets/error_state_widget.dart';
class TodayScreen extends ConsumerWidget {
const TodayScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todayStatsAsync = ref.watch(todayStatsProvider);
final topAppsAsync = ref.watch(todayTopAppsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('AutoTime Tracker'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.invalidate(todayStatsProvider);
ref.invalidate(todayTopAppsProvider);
},
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
// 设置页面通过底部导航栏访问
},
),
],
),
body: todayStatsAsync.when(
data: (stats) {
// 检查是否为空数据总时长为0且没有应用数据
final isEmpty = stats.totalTime == 0;
if (isEmpty) {
return _buildEmptyContent(context, ref);
}
return _buildContent(context, ref, stats, topAppsAsync);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorStateWidget.dataLoad(
message: _getErrorMessage(error),
onRetry: () {
ref.invalidate(todayStatsProvider);
ref.invalidate(todayTopAppsProvider);
},
),
),
);
}
Widget _buildContent(
BuildContext context,
WidgetRef ref,
DailyStats stats,
AsyncValue<List<AppUsage>> topAppsAsync,
) {
final theme = Theme.of(context);
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(todayStatsProvider);
ref.invalidate(todayTopAppsProvider);
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 总时长显示
_buildTotalTimeSection(stats, theme),
const SizedBox(height: 24),
// 效率评分
_buildEfficiencySection(stats, theme),
const SizedBox(height: 24),
// 分类时间分布(饼图)
_buildCategoryChart(stats, theme),
const SizedBox(height: 24),
// 分类标签
_buildCategoryTags(theme),
const SizedBox(height: 24),
// Top 应用列表
_buildTopAppsSection(context, ref, theme, topAppsAsync),
],
),
),
);
}
Widget _buildTotalTimeSection(DailyStats stats, ThemeData theme) {
return Center(
child: Column(
children: [
Text(
stats.formattedTotalTime,
style: theme.textTheme.displayLarge?.copyWith(
fontSize: 48,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
'今日总时长',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
);
}
Widget _buildEfficiencySection(DailyStats stats, ThemeData theme) {
final score = stats.efficiencyScore ?? 0;
final color = stats.efficiencyColor;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'效率评分',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
'$score%',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(width: 8),
...List.generate(5, (index) {
return Icon(
index < (score / 20).floor()
? Icons.star
: Icons.star_border,
size: 20,
color: color,
);
}),
],
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_getEfficiencyText(score),
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
String _getEfficiencyText(int score) {
if (score >= 80) return '优秀';
if (score >= 60) return '良好';
if (score >= 40) return '一般';
return '需改进';
}
Widget _buildCategoryChart(DailyStats stats, ThemeData theme) {
final categoryData = [
{'category': 'work', 'time': stats.workTime, 'color': AppTheme.workColor},
{'category': 'study', 'time': stats.studyTime, 'color': AppTheme.studyColor},
{'category': 'entertainment', 'time': stats.entertainmentTime, 'color': AppTheme.entertainmentColor},
{'category': 'social', 'time': stats.socialTime, 'color': AppTheme.socialColor},
{'category': 'tool', 'time': stats.toolTime, 'color': AppTheme.toolColor},
].where((item) => item['time'] as int > 0).toList();
return Container(
height: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'分类时间分布',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Expanded(
child: PieChart(
PieChartData(
sections: categoryData.map((item) {
final time = item['time'] as int;
final total = stats.totalTime;
final percentage = (time / total * 100);
return PieChartSectionData(
value: time.toDouble(),
title: '${percentage.toStringAsFixed(1)}%',
color: item['color'] as Color,
radius: 80,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
}).toList(),
sectionsSpace: 2,
centerSpaceRadius: 60,
),
),
),
],
),
);
}
Widget _buildCategoryTags(ThemeData theme) {
final categories = ['work', 'study', 'entertainment', 'social', 'tool'];
return Wrap(
spacing: 8,
runSpacing: 8,
children: categories.map((category) {
return FilterChip(
label: Text(AppTheme.getCategoryName(category)),
selected: false,
onSelected: (selected) {
// 筛选该分类
},
backgroundColor: AppTheme.getCategoryColor(category).withOpacity(0.1),
selectedColor: AppTheme.getCategoryColor(category),
labelStyle: TextStyle(
color: AppTheme.getCategoryColor(category),
fontWeight: FontWeight.w500,
),
);
}).toList(),
);
}
Widget _buildTopAppsSection(BuildContext context, WidgetRef ref, ThemeData theme, AsyncValue<List<AppUsage>> topAppsAsync) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Top Apps Today',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
topAppsAsync.when(
data: (apps) {
if (apps.isEmpty) {
return EmptyStateWidget.noApps(
onAction: () {
ref.invalidate(todayTopAppsProvider);
},
);
}
return Column(
children: apps.map((app) => _buildAppItem(app, theme)).toList(),
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
),
error: (error, stack) => Padding(
padding: const EdgeInsets.all(16),
child: ErrorStateWidget.dataLoad(
message: _getErrorMessage(error),
onRetry: () {
ref.invalidate(todayTopAppsProvider);
},
),
),
),
],
);
}
Widget _buildEmptyContent(BuildContext context, WidgetRef ref) {
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(todayStatsProvider);
ref.invalidate(todayTopAppsProvider);
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: EmptyStateWidget.firstTime(
onAction: () {
// 可以导航到权限设置页面
Navigator.of(context).pushNamed('/permission');
},
),
),
);
}
String _getErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('network') || errorString.contains('网络')) {
return '网络连接失败,请检查网络';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败';
}
return '加载失败,请稍后重试';
}
Widget _buildAppItem(AppUsage app, ThemeData theme) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(app.category).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.phone_android,
color: AppTheme.getCategoryColor(app.category),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
app.appName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
AppTheme.getCategoryName(app.category),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
Text(
app.formattedDuration,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'dart:async';
import 'time_tracking_service.dart';
import 'statistics_service.dart';
/// 后台同步服务 - 定期同步应用使用数据
class BackgroundSyncService {
final TimeTrackingService _timeTrackingService = TimeTrackingService();
final StatisticsService _statisticsService = StatisticsService();
Timer? _syncTimer;
bool _isRunning = false;
/// 启动后台同步
Future<void> start() async {
if (_isRunning) return;
_isRunning = true;
// 立即同步一次
await syncNow();
// 每 15 分钟同步一次
_syncTimer = Timer.periodic(const Duration(minutes: 15), (timer) async {
await syncNow();
});
// 启动原生后台追踪
await _timeTrackingService.startBackgroundTracking();
}
/// 停止后台同步
Future<void> stop() async {
_isRunning = false;
_syncTimer?.cancel();
_syncTimer = null;
await _timeTrackingService.stopBackgroundTracking();
}
/// 立即同步
Future<void> syncNow() async {
try {
print('Background sync: Starting sync...');
// 同步今日数据
await _timeTrackingService.syncTodayData();
// 刷新统计
await _statisticsService.refreshTodayStats();
print('Background sync: Completed');
} catch (e) {
print('Background sync error: $e');
}
}
/// 检查是否正在运行
bool get isRunning => _isRunning;
}

View File

@@ -0,0 +1,183 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:sqflite/sqflite.dart';
import '../database/database_helper.dart';
class CategoryService {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// Web 平台使用内存存储
final Map<String, String> _webCustomCategories = {};
// 预设分类规则
static const Map<String, String> defaultCategories = {
// 工作类
'com.microsoft.Office.Word': 'work',
'com.microsoft.Office.Excel': 'work',
'com.microsoft.Office.PowerPoint': 'work',
'com.slack': 'work',
'com.notion.Notion': 'work',
'com.figma.Figma': 'work',
'com.github': 'work',
'com.microsoft.VSCode': 'work',
'com.jetbrains': 'work',
'com.google.android.apps.docs': 'work',
'com.google.android.apps.sheets': 'work',
// 学习类
'com.coursera': 'study',
'com.khanacademy': 'study',
'com.amazon.kindle': 'study',
'com.gingerlabs.Notability': 'study',
'com.goodnotes': 'study',
'com.anki': 'study',
'com.duolingo': 'study',
// 娱乐类
'com.google.YouTube': 'entertainment',
'com.netflix.Netflix': 'entertainment',
'com.spotify.music': 'entertainment',
'com.spotify.client': 'entertainment',
'com.disney': 'entertainment',
// 社交类
'net.whatsapp.WhatsApp': 'social',
'com.instagram': 'social',
'com.twitter': 'social',
'com.facebook.Facebook': 'social',
'com.tencent.mm': 'social', // 微信
'com.tencent.mobileqq': 'social', // QQ
// 工具类
'com.apple.Safari': 'tool',
'com.android.chrome': 'tool',
'com.google.chrome': 'tool',
'com.microsoft.edge': 'tool',
'com.apple.mobilemail': 'tool',
'com.google.Gmail': 'tool',
};
// 获取应用分类
Future<String> getCategory(String packageName) async {
// 1. 先查用户自定义分类
final customCategory = await _getCustomCategory(packageName);
if (customCategory != null) {
return customCategory;
}
// 2. 查系统预设分类
if (defaultCategories.containsKey(packageName)) {
return defaultCategories[packageName]!;
}
// 3. 默认分类
return 'other';
}
// 获取用户自定义分类
Future<String?> _getCustomCategory(String packageName) async {
// Web 平台使用内存存储
if (kIsWeb) {
return _webCustomCategories[packageName];
}
final db = await _dbHelper.database;
final maps = await db.query(
'app_category',
where: 'package_name = ? AND is_custom = 1',
whereArgs: [packageName],
);
if (maps.isEmpty) return null;
return maps.first['category'] as String;
}
// 设置自定义分类
Future<void> setCategory(String packageName, String category) async {
// Web 平台使用内存存储
if (kIsWeb) {
_webCustomCategories[packageName] = category;
return;
}
final db = await _dbHelper.database;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await db.insert(
'app_category',
{
'package_name': packageName,
'category': category,
'is_custom': 1,
'created_at': now,
'updated_at': now,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// 删除自定义分类(恢复为系统默认)
Future<void> removeCustomCategory(String packageName) async {
// Web 平台使用内存存储
if (kIsWeb) {
_webCustomCategories.remove(packageName);
return;
}
final db = await _dbHelper.database;
await db.delete(
'app_category',
where: 'package_name = ? AND is_custom = 1',
whereArgs: [packageName],
);
}
// 获取所有自定义分类
Future<Map<String, String>> getAllCustomCategories() async {
// Web 平台使用内存存储
if (kIsWeb) {
return Map<String, String>.from(_webCustomCategories);
}
final db = await _dbHelper.database;
final maps = await db.query(
'app_category',
where: 'is_custom = 1',
);
final result = <String, String>{};
for (final map in maps) {
result[map['package_name'] as String] = map['category'] as String;
}
return result;
}
// 批量设置分类
Future<void> batchSetCategories(Map<String, String> categories) async {
// Web 平台使用内存存储
if (kIsWeb) {
_webCustomCategories.addAll(categories);
return;
}
final db = await _dbHelper.database;
final batch = db.batch();
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
for (final entry in categories.entries) {
batch.insert(
'app_category',
{
'package_name': entry.key,
'category': entry.value,
'is_custom': 1,
'created_at': now,
'updated_at': now,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
}
}

View File

@@ -0,0 +1,156 @@
import '../database/app_usage_dao.dart';
import '../database/daily_stats_dao.dart';
import '../theme/app_theme.dart';
class ExportService {
final AppUsageDao _appUsageDao = AppUsageDao();
final DailyStatsDao _dailyStatsDao = DailyStatsDao();
/// 导出 CSV 格式数据
Future<String> exportToCSV({
required DateTime startDate,
required DateTime endDate,
}) async {
final appUsages = await _appUsageDao.getAppUsages(
startTime: startDate,
endTime: endDate,
);
final buffer = StringBuffer();
// CSV 头部
buffer.writeln('应用名称,包名,开始时间,结束时间,使用时长(秒),使用时长(格式化),分类');
// 数据行
for (final usage in appUsages) {
buffer.writeln([
_escapeCsvField(usage.appName),
_escapeCsvField(usage.packageName),
usage.startTime.toIso8601String(),
usage.endTime.toIso8601String(),
usage.duration.toString(),
usage.formattedDuration,
AppTheme.getCategoryName(usage.category),
].join(','));
}
return buffer.toString();
}
/// 导出统计报告(文本格式)
Future<String> exportStatsReport({
required DateTime startDate,
required DateTime endDate,
}) async {
final stats = await _dailyStatsDao.getStatsRange(
startDate: startDate,
endDate: endDate,
);
final buffer = StringBuffer();
buffer.writeln('=== AutoTime Tracker 统计报告 ===');
buffer.writeln('报告时间: ${startDate.toString().split(' ')[0]}${endDate.toString().split(' ')[0]}');
buffer.writeln('');
buffer.writeln('日期统计:');
buffer.writeln('');
int totalWorkTime = 0;
int totalStudyTime = 0;
int totalEntertainmentTime = 0;
int totalSocialTime = 0;
int totalToolTime = 0;
int totalTime = 0;
for (final stat in stats) {
buffer.writeln('${stat.date.toString().split(' ')[0]}:');
buffer.writeln(' 总时长: ${stat.formattedTotalTime}');
buffer.writeln(' 工作: ${_formatTime(stat.workTime)}');
buffer.writeln(' 学习: ${_formatTime(stat.studyTime)}');
buffer.writeln(' 娱乐: ${_formatTime(stat.entertainmentTime)}');
buffer.writeln(' 社交: ${_formatTime(stat.socialTime)}');
buffer.writeln(' 工具: ${_formatTime(stat.toolTime)}');
if (stat.efficiencyScore != null) {
buffer.writeln(' 效率评分: ${stat.efficiencyScore}%');
}
buffer.writeln('');
totalWorkTime += stat.workTime;
totalStudyTime += stat.studyTime;
totalEntertainmentTime += stat.entertainmentTime;
totalSocialTime += stat.socialTime;
totalToolTime += stat.toolTime;
totalTime += stat.totalTime;
}
buffer.writeln('总计:');
buffer.writeln(' 总时长: ${_formatTime(totalTime)}');
buffer.writeln(' 工作: ${_formatTime(totalWorkTime)} (${(totalWorkTime / totalTime * 100).toStringAsFixed(1)}%)');
buffer.writeln(' 学习: ${_formatTime(totalStudyTime)} (${(totalStudyTime / totalTime * 100).toStringAsFixed(1)}%)');
buffer.writeln(' 娱乐: ${_formatTime(totalEntertainmentTime)} (${(totalEntertainmentTime / totalTime * 100).toStringAsFixed(1)}%)');
buffer.writeln(' 社交: ${_formatTime(totalSocialTime)} (${(totalSocialTime / totalTime * 100).toStringAsFixed(1)}%)');
buffer.writeln(' 工具: ${_formatTime(totalToolTime)} (${(totalToolTime / totalTime * 100).toStringAsFixed(1)}%)');
return buffer.toString();
}
/// 导出今日报告
Future<String> exportTodayReport() async {
final today = DateTime.now();
final startOfDay = DateTime(today.year, today.month, today.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
final stats = await _dailyStatsDao.getTodayStats();
final topApps = await _appUsageDao.getTopApps(
startTime: startOfDay,
endTime: endOfDay,
limit: 10,
);
final buffer = StringBuffer();
buffer.writeln('=== AutoTime Tracker 今日报告 ===');
buffer.writeln('日期: ${today.toString().split(' ')[0]}');
buffer.writeln('');
if (stats != null) {
buffer.writeln('总时长: ${stats.formattedTotalTime}');
buffer.writeln('工作: ${_formatTime(stats.workTime)}');
buffer.writeln('学习: ${_formatTime(stats.studyTime)}');
buffer.writeln('娱乐: ${_formatTime(stats.entertainmentTime)}');
buffer.writeln('社交: ${_formatTime(stats.socialTime)}');
buffer.writeln('工具: ${_formatTime(stats.toolTime)}');
if (stats.efficiencyScore != null) {
buffer.writeln('效率评分: ${stats.efficiencyScore}%');
}
buffer.writeln('');
}
if (topApps.isNotEmpty) {
buffer.writeln('Top 应用:');
for (int i = 0; i < topApps.length; i++) {
final app = topApps[i];
buffer.writeln('${i + 1}. ${app.appName}: ${app.formattedDuration} (${AppTheme.getCategoryName(app.category)})');
}
}
return buffer.toString();
}
String _escapeCsvField(String field) {
if (field.contains(',') || field.contains('"') || field.contains('\n')) {
return '"${field.replaceAll('"', '""')}"';
}
return field;
}
String _formatTime(int seconds) {
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
if (hours > 0) {
return '${hours}h ${minutes}m';
}
return '${minutes}m';
}
}

View File

@@ -0,0 +1,114 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../models/daily_stats.dart';
import '../models/app_usage.dart';
/// 测试数据服务 - 用于 Web 平台或开发测试
class MockDataService {
/// 生成今日测试统计数据
static DailyStats generateTodayStats() {
return DailyStats(
date: DateTime.now(),
totalTime: 23040, // 6小时24分钟
workTime: 14400, // 4小时
studyTime: 3600, // 1小时
entertainmentTime: 3600, // 1小时
socialTime: 1800, // 30分钟
toolTime: 0,
efficiencyScore: 72,
focusScore: 65,
appSwitchCount: 45,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
/// 生成本周测试统计数据
static List<DailyStats> generateWeekStats() {
final now = DateTime.now();
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
return List.generate(7, (index) {
final date = startOfWeek.add(Duration(days: index));
final baseTime = 20000 + (index * 500); // 每天略有不同
return DailyStats(
date: date,
totalTime: baseTime,
workTime: (baseTime * 0.6).round(), // 60% 工作时间
studyTime: (baseTime * 0.15).round(), // 15% 学习时间
entertainmentTime: (baseTime * 0.15).round(), // 15% 娱乐时间
socialTime: (baseTime * 0.08).round(), // 8% 社交时间
toolTime: (baseTime * 0.02).round(), // 2% 工具时间
efficiencyScore: 65 + (index * 2), // 效率评分递增
focusScore: 60 + (index * 3),
appSwitchCount: 40 + (index * 2),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
});
}
/// 生成今日 Top 应用测试数据
static List<AppUsage> generateTopApps() {
final now = DateTime.now();
return [
AppUsage(
packageName: 'com.google.chrome',
appName: 'Chrome',
startTime: now.subtract(const Duration(hours: 2, minutes: 15)),
endTime: now,
duration: 8100, // 2小时15分钟
category: 'work',
createdAt: now,
updatedAt: now,
),
AppUsage(
packageName: 'com.microsoft.vscode',
appName: 'VS Code',
startTime: now.subtract(const Duration(hours: 1, minutes: 30)),
endTime: now,
duration: 5400, // 1小时30分钟
category: 'work',
createdAt: now,
updatedAt: now,
),
AppUsage(
packageName: 'com.slack',
appName: 'Slack',
startTime: now.subtract(const Duration(hours: 1)),
endTime: now,
duration: 3600, // 1小时
category: 'work',
createdAt: now,
updatedAt: now,
),
AppUsage(
packageName: 'com.notion',
appName: 'Notion',
startTime: now.subtract(const Duration(minutes: 45)),
endTime: now,
duration: 2700, // 45分钟
category: 'work',
createdAt: now,
updatedAt: now,
),
AppUsage(
packageName: 'com.apple.mail',
appName: 'Mail',
startTime: now.subtract(const Duration(minutes: 30)),
endTime: now,
duration: 1800, // 30分钟
category: 'work',
createdAt: now,
updatedAt: now,
),
];
}
/// 检查是否应该使用测试数据
static bool shouldUseMockData() {
return kIsWeb;
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../models/daily_stats.dart';
import '../models/app_usage.dart';
import '../database/app_usage_dao.dart';
import '../database/daily_stats_dao.dart';
import 'mock_data_service.dart';
class StatisticsService {
final AppUsageDao _appUsageDao = AppUsageDao();
final DailyStatsDao _dailyStatsDao = DailyStatsDao();
// 获取今日统计(如果不存在则计算)
Future<DailyStats> getTodayStats() async {
// Web 平台返回测试数据
if (kIsWeb) {
return MockDataService.generateTodayStats();
}
final today = DateTime.now();
// 先查数据库
var stats = await _dailyStatsDao.getTodayStats();
if (stats != null) {
return stats;
}
// 如果不存在,计算并保存
stats = await _calculateDailyStats(today);
if (stats != null) {
await _dailyStatsDao.upsertDailyStats(stats);
} else {
// 如果没有数据,返回空统计
stats = DailyStats(
date: today,
totalTime: 0,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
return stats;
}
// 计算指定日期的统计
Future<DailyStats?> _calculateDailyStats(DateTime date) async {
final startOfDay = DateTime(date.year, date.month, date.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
// 获取该日期的所有应用使用记录
final appUsages = await _appUsageDao.getAppUsages(
startTime: startOfDay,
endTime: endOfDay,
);
if (appUsages.isEmpty) return null;
// 按分类聚合
final categoryTime = <String, int>{};
int totalTime = 0;
int appSwitchCount = 0;
for (final usage in appUsages) {
categoryTime[usage.category] = (categoryTime[usage.category] ?? 0) + usage.duration;
totalTime += usage.duration;
appSwitchCount += usage.deviceUnlockCount;
}
// 计算效率评分
final efficiencyScore = _calculateEfficiencyScore(categoryTime, totalTime);
// 计算专注度评分
final focusScore = _calculateFocusScore(appSwitchCount, totalTime);
return DailyStats(
date: startOfDay,
totalTime: totalTime,
workTime: categoryTime['work'] ?? 0,
studyTime: categoryTime['study'] ?? 0,
entertainmentTime: categoryTime['entertainment'] ?? 0,
socialTime: categoryTime['social'] ?? 0,
toolTime: categoryTime['tool'] ?? 0,
efficiencyScore: efficiencyScore,
focusScore: focusScore,
appSwitchCount: appSwitchCount,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
// 计算效率评分
int _calculateEfficiencyScore(Map<String, int> categoryTime, int totalTime) {
if (totalTime == 0) return 0;
// 工作时间占比40%
final workRatio = (categoryTime['work'] ?? 0) / totalTime;
final workScore = workRatio * 40;
// 学习时间占比30%
final studyRatio = (categoryTime['study'] ?? 0) / totalTime;
final studyScore = studyRatio * 30;
// 娱乐时间占比越低越好30%
final entertainmentRatio = (categoryTime['entertainment'] ?? 0) / totalTime;
final entertainmentScore = (1 - entertainmentRatio) * 30;
return (workScore + studyScore + entertainmentScore).round();
}
// 计算专注度评分
int _calculateFocusScore(int appSwitchCount, int totalTime) {
if (totalTime == 0) return 0;
// 平均每小时切换次数
final switchesPerHour = (appSwitchCount / (totalTime / 3600));
// 理想情况:每小时切换 < 10 次 = 100分
// 每小时切换 > 50 次 = 0分
final score = 100 - (switchesPerHour * 2).clamp(0, 100);
return score.round();
}
// 获取本周统计
Future<List<DailyStats>> getWeekStats() async {
// Web 平台返回测试数据
if (kIsWeb) {
return MockDataService.generateWeekStats();
}
final stats = await _dailyStatsDao.getWeekStats();
// 如果数据不完整,计算缺失的日期
final now = DateTime.now();
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
final result = <DailyStats>[];
for (int i = 0; i < 7; i++) {
final date = startOfWeek.add(Duration(days: i));
final existing = stats.firstWhere(
(s) => s.date.year == date.year &&
s.date.month == date.month &&
s.date.day == date.day,
orElse: () => DailyStats(
date: date,
totalTime: 0,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
result.add(existing);
}
return result;
}
// 获取本月统计
Future<List<DailyStats>> getMonthStats() async {
return await _dailyStatsDao.getMonthStats();
}
// 刷新今日统计(重新计算)
Future<DailyStats> refreshTodayStats() async {
final today = DateTime.now();
final stats = await _calculateDailyStats(today);
if (stats != null) {
await _dailyStatsDao.upsertDailyStats(stats);
return stats;
} else {
final emptyStats = DailyStats(
date: today,
totalTime: 0,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await _dailyStatsDao.upsertDailyStats(emptyStats);
return emptyStats;
}
}
// 获取 Top 应用
Future<List<AppUsage>> getTopApps({
required DateTime startTime,
required DateTime endTime,
int limit = 10,
}) async {
// Web 平台返回测试数据
if (kIsWeb) {
return MockDataService.generateTopApps().take(limit).toList();
}
return await _appUsageDao.getTopApps(
startTime: startTime,
endTime: endTime,
limit: limit,
);
}
}

View File

@@ -0,0 +1,182 @@
import 'package:flutter/services.dart';
import '../models/app_usage.dart';
import '../database/app_usage_dao.dart';
import 'category_service.dart';
import 'statistics_service.dart';
class TimeTrackingService {
static const MethodChannel _channel = MethodChannel('autotime_tracker/time_tracking');
final AppUsageDao _appUsageDao = AppUsageDao();
final CategoryService _categoryService = CategoryService();
final StatisticsService _statisticsService = StatisticsService();
/// 检查权限状态
Future<bool> hasPermission() async {
try {
final result = await _channel.invokeMethod<bool>('hasPermission');
return result ?? false;
} catch (e) {
print('Error checking permission: $e');
return false;
}
}
/// 请求权限
Future<bool> requestPermission() async {
try {
final result = await _channel.invokeMethod<bool>('requestPermission');
return result ?? false;
} catch (e) {
print('Error requesting permission: $e');
return false;
}
}
/// 获取应用使用数据(从系统 API
Future<List<AppUsageData>> getAppUsageFromSystem({
required DateTime startTime,
required DateTime endTime,
}) async {
try {
final result = await _channel.invokeMethod<List<dynamic>>('getAppUsage', {
'startTime': startTime.millisecondsSinceEpoch,
'endTime': endTime.millisecondsSinceEpoch,
});
if (result == null) return [];
return result
.map((item) => AppUsageData.fromMap(Map<String, dynamic>.from(item)))
.toList();
} catch (e) {
print('Error getting app usage from system: $e');
return [];
}
}
/// 同步应用使用数据到数据库
Future<void> syncAppUsageToDatabase({
required DateTime startTime,
required DateTime endTime,
}) async {
try {
// 1. 从系统获取数据
final systemData = await getAppUsageFromSystem(
startTime: startTime,
endTime: endTime,
);
if (systemData.isEmpty) {
print('No app usage data from system');
return;
}
// 2. 转换为 AppUsage 模型并分类
final appUsages = <AppUsage>[];
for (final data in systemData) {
// 获取分类
final category = await _categoryService.getCategory(data.packageName);
// 创建 AppUsage 对象
final usage = AppUsage(
packageName: data.packageName,
appName: data.appName,
startTime: data.startTime,
endTime: data.endTime,
duration: data.duration,
category: category,
deviceUnlockCount: data.deviceUnlockCount ?? 0,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
appUsages.add(usage);
}
// 3. 批量插入数据库
if (appUsages.isNotEmpty) {
await _appUsageDao.batchInsertAppUsages(appUsages);
print('Synced ${appUsages.length} app usage records to database');
}
// 4. 更新今日统计
await _statisticsService.refreshTodayStats();
} catch (e) {
print('Error syncing app usage to database: $e');
rethrow;
}
}
/// 同步今日数据
Future<void> syncTodayData() async {
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
await syncAppUsageToDatabase(
startTime: startOfDay,
endTime: endOfDay,
);
}
/// 启动后台追踪
Future<void> startBackgroundTracking() async {
try {
await _channel.invokeMethod('startBackgroundTracking');
} catch (e) {
print('Error starting background tracking: $e');
}
}
/// 停止后台追踪
Future<void> stopBackgroundTracking() async {
try {
await _channel.invokeMethod('stopBackgroundTracking');
} catch (e) {
print('Error stopping background tracking: $e');
}
}
/// 检查后台追踪状态
Future<bool> isBackgroundTrackingActive() async {
try {
final result = await _channel.invokeMethod<bool>('isBackgroundTrackingActive');
return result ?? false;
} catch (e) {
print('Error checking background tracking status: $e');
return false;
}
}
}
/// 系统返回的应用使用数据模型
class AppUsageData {
final String packageName;
final String appName;
final DateTime startTime;
final DateTime endTime;
final int duration; // 秒
final int? deviceUnlockCount;
AppUsageData({
required this.packageName,
required this.appName,
required this.startTime,
required this.endTime,
required this.duration,
this.deviceUnlockCount,
});
factory AppUsageData.fromMap(Map<String, dynamic> map) {
return AppUsageData(
packageName: map['packageName'] as String,
appName: map['appName'] as String,
startTime: DateTime.fromMillisecondsSinceEpoch(map['startTime'] as int),
endTime: DateTime.fromMillisecondsSinceEpoch(map['endTime'] as int),
duration: map['duration'] as int,
deviceUnlockCount: map['deviceUnlockCount'] as int?,
);
}
}

119
lib/theme/app_theme.dart Normal file
View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
// 颜色系统
static const Color primaryColor = Color(0xFF6366F1); // Indigo
static const Color secondaryColor = Color(0xFF8B5CF6); // Purple
static const Color accentColor = Color(0xFF10B981); // Green
// 分类颜色
static const Color workColor = Color(0xFF6366F1); // Indigo
static const Color studyColor = Color(0xFF8B5CF6); // Purple
static const Color entertainmentColor = Color(0xFFF59E0B); // Orange
static const Color socialColor = Color(0xFFEC4899); // Pink
static const Color toolColor = Color(0xFF6B7280); // Gray
// 状态颜色
static const Color successColor = Color(0xFF10B981);
static const Color warningColor = Color(0xFFF59E0B);
static const Color errorColor = Color(0xFFEF4444);
static const Color infoColor = Color(0xFF3B82F6);
// 浅色主题
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
),
textTheme: GoogleFonts.interTextTheme(),
scaffoldBackgroundColor: Colors.white,
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.grey[50],
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
titleTextStyle: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
);
}
// 深色主题
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme),
scaffoldBackgroundColor: const Color(0xFF1F2937),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: const Color(0xFF374151),
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: const Color(0xFF1F2937),
foregroundColor: Colors.white,
titleTextStyle: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
);
}
// 获取分类颜色
static Color getCategoryColor(String category) {
switch (category.toLowerCase()) {
case 'work':
return workColor;
case 'study':
return studyColor;
case 'entertainment':
return entertainmentColor;
case 'social':
return socialColor;
case 'tool':
return toolColor;
default:
return Colors.grey;
}
}
// 获取分类中文名称
static String getCategoryName(String category) {
switch (category.toLowerCase()) {
case 'work':
return '工作';
case 'study':
return '学习';
case 'entertainment':
return '娱乐';
case 'social':
return '社交';
case 'tool':
return '工具';
default:
return '其他';
}
}
}

View File

@@ -0,0 +1,241 @@
import 'package:flutter/material.dart';
/// 自定义时间选择器对话框
/// 用于选择小时和分钟,不会自动切换选择模式
class CustomTimePickerDialog extends StatefulWidget {
final String title;
final TimeOfDay initialTime;
const CustomTimePickerDialog({
super.key,
required this.title,
required this.initialTime,
});
@override
State<CustomTimePickerDialog> createState() => _CustomTimePickerDialogState();
/// 显示时间选择器对话框
static Future<TimeOfDay?> show({
required BuildContext context,
required String title,
required TimeOfDay initialTime,
}) async {
return await showDialog<TimeOfDay>(
context: context,
builder: (context) => CustomTimePickerDialog(
title: title,
initialTime: initialTime,
),
);
}
}
class _CustomTimePickerDialogState extends State<CustomTimePickerDialog> {
late int _hours;
late int _minutes;
@override
void initState() {
super.initState();
_hours = widget.initialTime.hour;
_minutes = widget.initialTime.minute;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Text(widget.title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 小时选择
Column(
children: [
Text('小时', style: theme.textTheme.bodySmall),
const SizedBox(height: 8),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
setState(() {
if (_hours > 0) {
_hours--;
} else {
_hours = 23;
}
});
},
),
Container(
width: 60,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_hours',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
if (_hours < 23) {
_hours++;
} else {
_hours = 0;
}
});
},
),
],
),
],
),
const SizedBox(width: 24),
// 分隔符
Text(
':',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 24),
// 分钟选择
Column(
children: [
Text('分钟', style: theme.textTheme.bodySmall),
const SizedBox(height: 8),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
setState(() {
if (_minutes >= 15) {
_minutes -= 15;
} else if (_minutes > 0) {
_minutes = 0;
} else {
_minutes = 45;
}
});
},
),
Container(
width: 60,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_minutes.toString().padLeft(2, '0'),
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
_minutes += 15;
if (_minutes >= 60) {
_minutes = 0;
}
});
},
),
],
),
],
),
],
),
const SizedBox(height: 16),
// 快速选择按钮
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
_buildQuickTimeButton(context, theme, '00:00', 0, 0),
_buildQuickTimeButton(context, theme, '08:00', 8, 0),
_buildQuickTimeButton(context, theme, '12:00', 12, 0),
_buildQuickTimeButton(context, theme, '18:00', 18, 0),
_buildQuickTimeButton(context, theme, '20:00', 20, 0),
_buildQuickTimeButton(context, theme, '22:00', 22, 0),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(
TimeOfDay(hour: _hours, minute: _minutes),
);
},
child: const Text('确定'),
),
],
);
}
Widget _buildQuickTimeButton(
BuildContext context,
ThemeData theme,
String label,
int hours,
int minutes,
) {
final isSelected = _hours == hours && _minutes == minutes;
return OutlinedButton(
onPressed: () {
setState(() {
_hours = hours;
_minutes = minutes;
});
},
style: OutlinedButton.styleFrom(
backgroundColor: isSelected
? theme.colorScheme.primaryContainer.withOpacity(0.3)
: null,
side: BorderSide(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.outline.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
),
),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// 空状态组件
/// 用于显示无数据时的友好提示
class EmptyStateWidget extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final String? actionLabel;
final VoidCallback? onAction;
final bool showIllustration;
const EmptyStateWidget({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.actionLabel,
this.onAction,
this.showIllustration = true,
});
/// 默认空状态 - 无数据
factory EmptyStateWidget.noData({
String? title,
String? subtitle,
String? actionLabel,
VoidCallback? onAction,
}) {
return EmptyStateWidget(
icon: Icons.inbox_outlined,
title: title ?? '暂无数据',
subtitle: subtitle ?? '使用应用一段时间后,数据将显示在这里',
actionLabel: actionLabel,
onAction: onAction,
);
}
/// 空状态 - 无搜索结果
factory EmptyStateWidget.noSearchResults({
String? query,
}) {
return EmptyStateWidget(
icon: Icons.search_off,
title: '未找到匹配结果',
subtitle: query != null ? '没有找到与 "$query" 相关的内容' : '请尝试其他关键词',
showIllustration: false,
);
}
/// 空状态 - 无应用数据
factory EmptyStateWidget.noApps({
VoidCallback? onAction,
}) {
return EmptyStateWidget(
icon: Icons.apps_outlined,
title: '暂无应用数据',
subtitle: '使用应用一段时间后,应用使用记录将显示在这里',
actionLabel: '刷新',
onAction: onAction,
);
}
/// 空状态 - 首次使用
factory EmptyStateWidget.firstTime({
VoidCallback? onAction,
}) {
return EmptyStateWidget(
icon: Icons.touch_app,
title: '欢迎使用 AutoTime Tracker',
subtitle: '开始追踪您的时间使用情况\n\n1. 授予应用使用权限\n2. 正常使用您的设备\n3. 查看详细的使用统计',
actionLabel: '去设置权限',
onAction: onAction,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showIllustration) ...[
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 64,
color: AppTheme.primaryColor.withOpacity(0.6),
),
),
const SizedBox(height: 24),
] else ...[
Icon(
icon,
size: 64,
color: theme.colorScheme.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
],
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
if (subtitle != null) ...[
const SizedBox(height: 12),
Text(
subtitle!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onAction,
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// 错误状态组件
/// 用于显示错误时的友好提示
class ErrorStateWidget extends StatelessWidget {
final String? title;
final String? message;
final String? actionLabel;
final VoidCallback? onRetry;
final IconData icon;
final bool showDetails;
const ErrorStateWidget({
super.key,
this.title,
this.message,
this.actionLabel,
this.onRetry,
this.icon = Icons.error_outline,
this.showDetails = false,
});
/// 默认错误状态
factory ErrorStateWidget.generic({
String? message,
VoidCallback? onRetry,
}) {
return ErrorStateWidget(
title: '加载失败',
message: message ?? '请检查网络连接后重试',
actionLabel: '重试',
onRetry: onRetry,
);
}
/// 权限错误
factory ErrorStateWidget.permission({
VoidCallback? onRetry,
}) {
return ErrorStateWidget(
icon: Icons.security,
title: '权限未授予',
message: '需要授予应用使用权限才能追踪时间\n\n请在系统设置中开启"使用情况访问权限"',
actionLabel: '去设置',
onRetry: onRetry,
);
}
/// 网络错误
factory ErrorStateWidget.network({
VoidCallback? onRetry,
}) {
return ErrorStateWidget(
icon: Icons.wifi_off,
title: '网络连接失败',
message: '无法连接到服务器\n\n请检查网络连接后重试',
actionLabel: '重试',
onRetry: onRetry,
);
}
/// 数据加载错误
factory ErrorStateWidget.dataLoad({
String? message,
VoidCallback? onRetry,
}) {
return ErrorStateWidget(
icon: Icons.cloud_off,
title: '数据加载失败',
message: message ?? '无法加载数据,请稍后重试',
actionLabel: '重试',
onRetry: onRetry,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppTheme.errorColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 56,
color: AppTheme.errorColor,
),
),
const SizedBox(height: 24),
Text(
title ?? '出错了',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.error,
),
textAlign: TextAlign.center,
),
if (message != null) ...[
const SizedBox(height: 12),
Text(
message!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
],
),
),
);
}
}