You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
8.2 KiB
260 lines
8.2 KiB
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;
|
|
}
|
|
}
|
|
}
|