实现数据库 #5

Merged
ps9up4ig6 merged 1 commits from gxh_branch into dev 2 years ago

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:timemanage/model/project.dart';
import 'package:timemanage/model/timer_entry.dart';
abstract class DataProvider {
Future<Project> createProject({required String name, Color? colour});
Future<List<Project>> listProjects();
Future<void> editProject(Project project);
Future<void> deleteProject(Project project);
Future<TimerEntry> createTimer({
String? description,
int? projectID,
DateTime? startTime,
DateTime? endTime,
});
Future<List<TimerEntry>> listTimers();
Future<void> editTimer(TimerEntry timer);
Future<void> deleteTimer(TimerEntry timer);
Future<void> import(DataProvider other) async {
List<TimerEntry> otherEntries = await other.listTimers();
List<Project> otherProjects = await other.listProjects();
List<Project> newOtherProjects = await Stream.fromIterable(otherProjects)
.asyncMap((event) => createProject(name: event.name))
.toList();
for (TimerEntry otherEntry in otherEntries) {
int projectOffset = otherProjects
.indexWhere((element) => element.id == otherEntry.projectID);
int? projectID;
if (projectOffset >= 0) {
projectID = newOtherProjects[projectOffset].id;
}
await createTimer(
description: otherEntry.description,
projectID: projectID,
startTime: otherEntry.startTime,
endTime: otherEntry.endTime,
);
}
}
}

@ -0,0 +1,259 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:timemanage/db/data_provider.dart';
import 'package:timemanage/model/timer_entry.dart';
import 'package:timemanage/model/project.dart';
import 'package:path/path.dart' as p;
import 'package:xdg_directories/xdg_directories.dart';
class DatabaseProvider extends DataProvider {
final Database _db;
static const int _dbVersion = 4;
DatabaseProvider(this._db);
Future<void> close() async {
await _db.close();
}
static void _onConfigure(Database db) async {
await db.execute("PRAGMA foreign_keys = OFF");
}
static void _onCreate(Database db, int version) async {
await db.execute('''
create table if not exists projects(
id integer not null primary key autoincrement,
name text not null,
colour int not null,
archived boolean not null default 0
)
''');
await db.execute('''
create table if not exists timers(
id integer not null primary key autoincrement,
project_id integer default null,
description text not null,
start_time int not null,
end_time int default null,
notes text default null,
foreign key(project_id) references projects(id) on delete set null
)
''');
await db.execute('''
create index if not exists timers_start_time on timers(start_time)
''');
}
static void _onUpgrade(Database db, int version, int newVersion) async {
if (version < 2) {
await db.execute('''
alter table projects add column archived boolean not null default false
''');
}
if (version < 3) {
await db.execute('''
alter table timers add column notes text default null
''');
}
if (version < 4) {
// fix the bug of the default value being `false` for project archives instead of `0`.
// `false` works fine on sqlite >= 3.23.0. Unfortunately, some Android phones still have
// ancient sqlite versions, so to them `false` is a string rather than an integer with
// value `0`
Batch b = db.batch();
b.execute('''
create table projects_tmp(
id integer not null primary key autoincrement,
name text not null,
colour int not null,
archived boolean not null default 0
)
''');
b.execute("insert into projects_tmp select * from projects");
b.execute("drop table projects");
b.execute('''
create table projects(
id integer not null primary key autoincrement,
name text not null,
colour int not null,
archived boolean not null default 0
)
''');
b.execute('''
insert into projects select id, name, colour,
case archived
when 'false' then 0
when 'true' then 1
when '0' then 0
when '1' then 1
when 0 then 0
when 1 then 1
else 0
end as archived
from projects_tmp
''');
b.execute("drop table projects_tmp");
await b.commit(noResult: true);
}
}
static Future<DatabaseProvider> open(String path) async {
// open the database
Database db = await openDatabase(path,
onConfigure: _onConfigure,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
version: _dbVersion);
await db.execute("PRAGMA foreign_keys = ON");
DatabaseProvider repo = DatabaseProvider(db);
return repo;
}
/// the c in crud
@override
Future<Project> createProject(
{required String name, Color? colour, bool? archived}) async {
colour ??= Color.fromARGB(255, 60, 108, 186);
archived ??= false;
int id = await _db.rawInsert(
"insert into projects(name, colour, archived) values(?, ?, ?)",
<dynamic>[name, colour.value, archived ? 1 : 0]);
return Project(id: id, name: name, colour: colour, archived: archived);
}
/// the r in crud
@override
Future<List<Project>> listProjects() async {
List<Map<String, dynamic>> rawProjects = await _db.rawQuery('''
select id, name, colour,
case archived
when 'false' then 0
when 'true' then 1
when '0' then 0
when '1' then 1
when 0 then 0
when 1 then 1
else 0
end as archived
from projects order by name asc
''');
return rawProjects
.map((Map<String, dynamic> row) => Project(
id: row["id"] as int,
name: row["name"] as String,
colour: Color(row["colour"] as int),
archived: (row["archived"] as int?) == 1))
.toList();
}
/// the u in crud
@override
Future<void> editProject(Project project) async {
int rows = await _db.rawUpdate(
"update projects set name=?, colour=?, archived=? where id=?",
<dynamic>[
project.name,
project.colour,
project.archived ? 1 : 0,
project.id
]);
assert(rows == 1);
}
/// the d in crud
@override
Future<void> deleteProject(Project project) async {
await _db
.rawDelete("delete from projects where id=?", <dynamic>[project.id]);
}
/// the c in crud
@override
Future<TimerEntry> createTimer(
{String? description,
int? projectID,
DateTime? startTime,
DateTime? endTime,
String? notes}) async {
int st = startTime?.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch;
int? et = endTime?.millisecondsSinceEpoch;
int id = await _db.rawInsert(
"insert into timers(project_id, description, start_time, end_time, notes) values(?, ?, ?, ?, ?)",
<dynamic>[projectID, description, st, et, notes]);
return TimerEntry(
id: id,
description: description,
projectID: projectID,
startTime: DateTime.fromMillisecondsSinceEpoch(st),
endTime: endTime,
notes: notes);
}
/// the r in crud
@override
Future<List<TimerEntry>> listTimers() async {
List<Map<String, dynamic>> rawTimers = await _db.rawQuery(
"select id, project_id, description, start_time, end_time, notes from timers order by start_time asc");
return rawTimers
.map((Map<String, dynamic> row) => TimerEntry(
id: row["id"] as int,
projectID: row["project_id"] as int?,
description: row["description"] as String?,
startTime:
DateTime.fromMillisecondsSinceEpoch(row["start_time"] as int),
endTime: row["end_time"] != null
? DateTime.fromMillisecondsSinceEpoch(row["end_time"] as int)
: null,
notes: row["notes"] as String?))
.toList();
}
/// the u in crud
@override
Future<void> editTimer(TimerEntry timer) async {
int st = timer.startTime.millisecondsSinceEpoch;
int? et = timer.endTime?.millisecondsSinceEpoch;
await _db.rawUpdate(
"update timers set project_id=?, description=?, start_time=?, end_time=?, notes=? where id=?",
<dynamic>[
timer.projectID,
timer.description,
st,
et,
timer.notes,
timer.id
]);
}
/// the d in crud
@override
Future<void> deleteTimer(TimerEntry timer) async {
await _db.rawDelete("delete from timers where id=?", <dynamic>[timer.id]);
}
static Future<File> getDatabaseFile() async {
final dbPath =
(Platform.isLinux) ? dataHome.path : await getDatabasesPath();
return File(p.join(dbPath, 'timecop.db'));
}
static Future<bool> isValidDatabaseFile(String path) async {
try {
Database db = await openDatabase(path, readOnly: true);
await db.rawQuery(
"select id, name, colour, archived from projects order by name asc limit 1");
await db.rawQuery(
"select id, project_id, description, start_time, end_time, notes from timers order by start_time asc limit 1");
await db.close();
return true;
} on Exception catch (_) {
return false;
}
}
}

@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:timemanage/db/data_provider.dart';
import 'package:timemanage/model/project.dart';
import 'package:timemanage/model/timer_entry.dart';
import 'dart:math';
class MockDataProvider extends DataProvider {
String localeKey;
static final Map<String, Map<String, String>> l10n = {
"en": {
"administration": "Administration",
"mockups": "Mockups",
"ui-layout": "UI Layout",
"coffee": "Coffee",
"app-development": "App development"
},
"zh-CN": {
"ui-layout": "UI布局",
"administration": "管理",
"coffee": "咖啡",
"mockups": "样机",
"app-development": "应用程式开发",
},
};
MockDataProvider(Locale locale) : localeKey = locale.languageCode {
if (locale.languageCode == "zh") {
localeKey += "-${locale.countryCode!}";
}
}
@override
Future<List<Project>> listProjects() async {
return <Project>[
Project(
id: 1,
name: "Time Manager",
colour: Colors.cyan[600]!,
archived: false),
Project(
id: 2,
name: l10n[localeKey]!["administration"]!,
colour: Colors.pink[600]!,
archived: false,
),
];
}
@override
Future<List<TimerEntry>> listTimers() async {
int tid = 1;
Random rand = Random(42);
// start with running timers
List<TimerEntry> entries = [
TimerEntry(
id: tid++,
description: l10n[localeKey]!["ui-layout"],
projectID: 1,
startTime: DateTime.now()
.subtract(const Duration(hours: 2, minutes: 10, seconds: 1)),
endTime: null,
),
TimerEntry(
id: tid++,
description: l10n[localeKey]!["coffee"],
projectID: 2,
startTime:
DateTime.now().subtract(const Duration(minutes: 3, seconds: 14)),
endTime: null,
),
];
// add some fake March stuff
for (int w = 0; w < 4; w++) {
for (int d = 0; d < 5; d++) {
String descriptionKey;
double r = rand.nextDouble();
if (r <= 0.2) {
descriptionKey = 'mockups';
} else if (r <= 0.5) {
descriptionKey = 'ui-layout';
} else {
descriptionKey = 'app-development';
}
entries.add(TimerEntry(
id: tid++,
description: l10n[localeKey]![descriptionKey],
projectID: 1,
startTime: DateTime(
2020,
3,
(w * 7) + d + 2,
rand.nextInt(3) + 8,
rand.nextInt(60),
rand.nextInt(60),
),
endTime: DateTime(
2020,
3,
(w * 7) + d + 2,
rand.nextInt(3) + 13,
rand.nextInt(60),
rand.nextInt(60),
),
));
entries.add(TimerEntry(
id: tid++,
description: l10n[localeKey]!['administration'],
projectID: 2,
startTime: DateTime(
2020,
3,
(w * 7) + d + 2,
14,
rand.nextInt(30),
rand.nextInt(60),
),
endTime: DateTime(
2020,
3,
(w * 7) + d + 2,
15,
rand.nextInt(30),
rand.nextInt(60),
),
));
}
}
return entries;
}
@override
Future<Project> createProject(
{required String name, Color? colour, bool? archived}) async {
return Project(
id: -1, name: name, colour: colour!, archived: archived ?? false);
}
@override
Future<void> editProject(Project project) async {}
@override
Future<void> deleteProject(Project project) async {}
@override
Future<TimerEntry> createTimer(
{String? description,
int? projectID,
DateTime? startTime,
DateTime? endTime}) async {
DateTime st = startTime ?? DateTime.now();
return TimerEntry(
id: -1,
description: description,
projectID: projectID,
startTime: st,
endTime: endTime,
);
}
@override
Future<void> editTimer(TimerEntry timer) async {}
@override
Future<void> deleteTimer(TimerEntry timer) async {}
}

@ -4,12 +4,14 @@ import 'package:equatable/equatable.dart'; // 比较两个对象是否相等的
class Project extends Equatable {
final int id;
final String name;
final Color? colour; //
final bool archived; //
//
const Project({
required this.id,
required this.name,
required this.colour,
required this.archived,
});
@ -24,6 +26,7 @@ class Project extends Equatable {
}) : this(
id: project.id,
name: name ?? project.name,
colour: color ?? project.colour,
archived: archived ?? project.archived,
);
}

@ -6,9 +6,11 @@ import FlutterMacOS
import Foundation
import package_info_plus
import sqflite
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

@ -333,6 +333,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
stack_trace:
dependency: transitive
description:
@ -357,6 +373,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
term_glyph:
dependency: transitive
description:
@ -493,6 +517,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.8"
xdg_directories:
dependency: "direct main"
description:
name: xdg_directories
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
xml:
dependency: transitive
description:

@ -37,6 +37,8 @@ dependencies:
font_awesome_flutter: ^9.2.0
url_launcher: ^6.1.14
equatable: ^2.0.3 # 用于对象比较
sqflite: ^2.0.0+3
xdg_directories: ^1.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

Loading…
Cancel
Save