first commit
This commit is contained in:
154
lib/database/app_usage_dao.dart
Normal file
154
lib/database/app_usage_dao.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
165
lib/database/daily_stats_dao.dart
Normal file
165
lib/database/daily_stats_dao.dart
Normal 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')}';
|
||||
}
|
||||
}
|
||||
|
||||
114
lib/database/database_helper.dart
Normal file
114
lib/database/database_helper.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
130
lib/database/time_goal_dao.dart
Normal file
130
lib/database/time_goal_dao.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user