计时器分类 #7

Merged
ps9up4ig6 merged 24 commits from gxh_branch into main 2 years ago

@ -18,18 +18,6 @@ migration:
- platform: android
create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
- platform: ios
create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
- platform: linux
create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
- platform: macos
create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
- platform: web
create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a
# User provided section

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

@ -1,11 +1,7 @@
<<<<<<< HEAD
=======
| 姓名 | 知士荟 | 头歌 | Github |
| ------ | -------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------- |
| 姓名 | 知士荟 | 头歌 | Github |
| ------ | ----------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- |
| 葛兴海 | [@葛兴海](https://www.learnerhub.net/#/users/12147/docs) | [@葛兴海](https://code.educoder.net/user/ps9up4ig6) | [@Sheeet](https://github.com/icesheeet) |
| 庞浩 | [@庞浩](https://www.learnerhub.net/#/users/12027/docs) | [@庞浩](https://code.educoder.net/user/mbhvfy6mx) | |
| 庞浩 | [@庞浩](https://www.learnerhub.net/#/users/12027/docs) | [@庞浩](https://code.educoder.net/user/mbhvfy6mx) | |
| 卫俊钢 | [@卫俊钢](https://www.learnerhub.net/#/users/12144/docs) | [@卫俊钢](https://www.educoder.net/users/p2jf6ytqz) | [@JungangWei](https://github.com/githubwjg) |
| 邹兴云 | [@邹兴云](https://www.learnerhub.net/#/users/12026/docs) | [@邹兴云](https://www.educoder.net/users/p8fjyvg3u) | |
| 蔡玉祥 | [@蔡玉祥](https://www.learnerhub.net/#/users/12015/docs) | [@蔡玉祥](https://www.educoder.net/users/mszfy297n) | |
>>>>>>> gxh_branch
| 邹兴云 | [@邹兴云](https://www.learnerhub.net/#/users/12026/docs) | [@邹兴云](https://www.educoder.net/users/p8fjyvg3u) | |
| 蔡玉祥 | [@蔡玉祥](https://www.learnerhub.net/#/users/12015/docs) | [@蔡玉祥](https://www.educoder.net/users/mszfy297n) | |

@ -1,2 +1,2 @@
#Tue Oct 24 13:39:52 CST 2023
gradle.version=7.4.2
#Sat Nov 04 11:50:30 CST 2023
gradle.version=7.5

@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
namespace "com.example.timemanage"
namespace "com.example.timemaneger"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
@ -42,7 +42,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.timemanage"
applicationId "com.example.timemaneger"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="timemanage"
android:label="timemaneger"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

@ -20,6 +20,11 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {

@ -1,4 +1,4 @@
package com.example.timemanage
package com.example.timemaneger
import io.flutter.embedding.android.FlutterActivity

@ -1,2 +1,5 @@
sdk.dir=C:\\Users\\26538\\AppData\\Local\\Android\\sdk
flutter.sdk=F:\\study\\school\\ruanjian\\flutter\\flutter
sdk.dir=C:\\Users\\28749\\AppData\\Local\\Android\\sdk
flutter.sdk=C:\\flutter
flutter.buildMode=debug
flutter.versionName=1.0.0
flutter.versionCode=1

@ -1,11 +1,7 @@
<<<<<<< HEAD
=======
| 姓名 | 知士荟 | 头歌 | Github |
| ------ | -------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------- |
| 姓名 | 知士荟 | 头歌 | Github |
| ------ | ----------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- |
| 葛兴海 | [@葛兴海](https://www.learnerhub.net/#/users/12147/docs) | [@葛兴海](https://code.educoder.net/user/ps9up4ig6) | [@Sheeet](https://github.com/icesheeet) |
| 庞浩 | [@庞浩](https://www.learnerhub.net/#/users/12027/docs) | [@庞浩](https://code.educoder.net/user/mbhvfy6mx) | |
| 庞浩 | [@庞浩](https://www.learnerhub.net/#/users/12027/docs) | [@庞浩](https://code.educoder.net/user/mbhvfy6mx) | |
| 卫俊钢 | [@卫俊钢](https://www.learnerhub.net/#/users/12144/docs) | [@卫俊钢](https://www.educoder.net/users/p2jf6ytqz) | [@JungangWei](https://github.com/githubwjg) |
| 邹兴云 | [@邹兴云](https://www.learnerhub.net/#/users/12026/docs) | [@邹兴云](https://www.educoder.net/users/p8fjyvg3u) | |
| 蔡玉祥 | [@蔡玉祥](https://www.learnerhub.net/#/users/12015/docs) | [@蔡玉祥](https://www.educoder.net/users/mszfy297n) | |
>>>>>>> gxh_branch
| 邹兴云 | [@邹兴云](https://www.learnerhub.net/#/users/12026/docs) | [@邹兴云](https://www.educoder.net/users/p8fjyvg3u) | |
| 蔡玉祥 | [@蔡玉祥](https://www.learnerhub.net/#/users/12015/docs) | [@蔡玉祥](https://www.educoder.net/users/mszfy297n) | |

@ -1986,6 +1986,31 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
bloc
flutter_bloc
The MIT License (MIT)
Copyright (c) 2018 Felix Angelov
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
boolean_selector
meta
@ -6836,6 +6861,9 @@ SOFTWARE.
--------------------------------------------------------------------------------
flutter_lints
flutter_markdown
path_provider
path_provider_android
path_provider_foundation
path_provider_linux
path_provider_platform_interface
path_provider_windows
@ -28922,6 +28950,36 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
intl
Copyright 2013, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
js
@ -30781,6 +30839,31 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
nested
provider
MIT License
Copyright (c) 2019 Remi Rousselet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
package_info_plus

@ -12,6 +12,12 @@
@import package_info_plus;
#endif
#if __has_include(<path_provider_foundation/PathProviderPlugin.h>)
#import <path_provider_foundation/PathProviderPlugin.h>
#else
@import path_provider_foundation;
#endif
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else
@ -34,6 +40,7 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FLTPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPackageInfoPlusPlugin"]];
[PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[FLTURLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTURLLauncherPlugin"]];

@ -1,44 +0,0 @@
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,
);
}
}
}

@ -1,259 +0,0 @@
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;
}
}
}

@ -1,166 +0,0 @@
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 {}
}

@ -0,0 +1,115 @@
import 'package:sqflite/sqflite.dart';
import 'package:timemanage/model/timer_entry.dart';
class TimerEntryDatabase {
static final TimerEntryDatabase instance = TimerEntryDatabase._init();
static Database? _database;
TimerEntryDatabase._init();
///
//
Future<Database> get database async {
if (_database != null) {
return _database!;
}
_database = await _initDB('timer_entry.db');
return _database!;
}
//
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = dbPath + filePath;
//
return await openDatabase(path, version: 1, onCreate: _createDB);
}
//
Future _createDB(Database db, int version) async {
//
const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
const textType = 'TEXT NOT NULL';
const nullTextType = 'TEXT';
//
await db.execute('''
CREATE TABLE $tableTimerEntry (
${TimerEntryFields.id} $idType,
${TimerEntryFields.name} $textType,
${TimerEntryFields.createdAt} $textType,
${TimerEntryFields.endAt} $nullTextType,
${TimerEntryFields.isActive} $textType,
${TimerEntryFields.stopWatch} $textType
)
''');
}
/// CRUD
//
Future<TimerEntry> create(TimerEntry timerEntry) async {
final db = await instance.database;
// ID
final id = await db.insert(tableTimerEntry, timerEntry.toJson());
return timerEntry.copy(id: id);
}
//
Future<TimerEntry> read(int id) async {
final db = await instance.database;
final maps = await db.query(tableTimerEntry,
columns: TimerEntryFields.values,
where: '${TimerEntryFields.id} = ?',
whereArgs: [id]);
if (maps.isNotEmpty) {
return TimerEntry.fromJson(maps.first);
} else {
throw Exception('ID $id not found');
}
}
//
Future<List<TimerEntry>> readAll() async {
final db = await instance.database;
//
// await db.insert(
// tableTimerEntry,
// TimerEntry(
// name: '测试计时器',
// ).toJson());
final result = await db.query(tableTimerEntry);
return result.map((json) => TimerEntry.fromJson(json)).toList();
}
//
Future<int> update(TimerEntry timerEntry) async {
final db = await instance.database;
return db.update(tableTimerEntry, timerEntry.toJson(),
where: '${TimerEntryFields.id} = ?', whereArgs: [timerEntry.id]);
}
//
Future<int> delete(int id) async {
final db = await instance.database;
return await db.delete(tableTimerEntry,
where: '${TimerEntryFields.id} = ?', whereArgs: [id]);
}
//
Future close() async {
final db = await instance.database;
db.close();
}
}

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:timemanage/screen/dashboard_screen.dart';
import 'package:timemanage/screen/dashboard/dashboard_screen.dart';
///
/// dartdoc/** */
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
@ -11,15 +13,18 @@ class MyApp extends StatelessWidget {
return MaterialApp(
//
theme: ThemeData(
//
menuBarTheme: MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.blueAccent),
),
),
menuTheme: MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.white)),
),
// //
// menuTheme: MenuThemeData(
// style: MenuStyle(
// backgroundColor:
// MaterialStatePropertyAll<Color>(Colors.lightBlueAccent)),
// ),
),
//
home: DashBoardScreen(),

@ -1,58 +1,106 @@
import 'package:equatable/equatable.dart';
const String tableTimerEntry = 'timer_entry'; //
//
class TimerEntry extends Equatable {
final int id;
final String? description;
final int? projectID;
final DateTime startTime;
final DateTime? endTime;
final String? notes;
//
class TimerEntryFields {
static const String id = '_id';
static const String name = 'name';
static const String createdAt = 'createdAt';
static const String endAt = 'endAt';
static const String isActive = 'isActive';
static const String stopWatch = 'stopWatch';
static final List<String> values = [
/// Add all fields
id, name, createdAt, endAt, isActive, stopWatch
];
}
class TimerEntry {
int? id; // ID
String name; //
Stopwatch? stopwatch; //
bool isActice; //
DateTime createdAt; //
DateTime? endAt; //
//
const TimerEntry(
{required this.id,
required this.description,
required this.projectID,
required this.startTime,
required this.endTime,
this.notes = ""});
//
@override
List<Object?> get props =>
[id, description, projectID, startTime, endTime, notes];
@override
bool get stringify => true;
//
TimerEntry.clone(TimerEntry timer,
{String? description,
int? projectID,
DateTime? startTime,
DateTime? endTime,
String? notes})
: this(
id: timer.id,
description: description ?? timer.description,
projectID: projectID ?? timer.projectID,
startTime: startTime ?? timer.startTime,
endTime: endTime ?? timer.endTime,
notes: notes ?? timer.notes,
);
//
static String formatDuration(Duration d) {
if (d.inHours > 0) {
return "${d.inHours}:${(d.inMinutes - (d.inHours * 60)).toString().padLeft(2, "0")}:${(d.inSeconds - (d.inMinutes * 60)).toString().padLeft(2, "0")}";
} else {
return "${d.inMinutes.toString().padLeft(2, "0")}:${(d.inSeconds - (d.inMinutes * 60)).toString().padLeft(2, "0")}";
}
TimerEntry({required this.name})
: stopwatch = Stopwatch(),
isActice = false,
createdAt = DateTime.now(),
endAt = DateTime.now();
bool get isActive => isActice;
num get stopWatch => stopWatch;
//
void start() {
stopwatch!.start();
isActice = true;
}
//
void pause() {
stopwatch!.stop();
isActice = false;
}
//
void stop() {
stopwatch!.stop();
isActice = false;
endAt = DateTime.now();
}
//
void reset() {
stopwatch!.reset();
isActice = false;
endAt = null;
}
//
TimerEntry copy({
int? id,
String? name,
DateTime? createdAt,
DateTime? endAt,
bool? isActive,
Stopwatch? stopwatch,
}) =>
TimerEntry(
name: name ?? this.name,
)
..id = id ?? this.id
..createdAt = createdAt ?? this.createdAt
..endAt = endAt ?? this.endAt
..isActice = isActive ?? this.isActice
..stopwatch = stopwatch ?? this.stopwatch;
// fromJson
static TimerEntry fromJson(Map<String, Object?> json) {
return TimerEntry(
name: json[TimerEntryFields.name] as String,
)
..id = json[TimerEntryFields.id] as int?
..createdAt = DateTime.parse(json[TimerEntryFields.createdAt] as String)
..endAt = json[TimerEntryFields.endAt] == null
? null
: DateTime.parse(json[TimerEntryFields.endAt] as String)
..isActice = json[TimerEntryFields.isActive] == 1 ? true : false
..stopwatch = Stopwatch();
}
//
String formatTime() {
Duration d = (endTime ?? DateTime.now()).difference(startTime);
return formatDuration(d);
// toJson
Map<String, Object?> toJson() {
return {
TimerEntryFields.id: id,
TimerEntryFields.name: name,
TimerEntryFields.createdAt: createdAt.toIso8601String(),
TimerEntryFields.endAt: endAt?.toIso8601String(),
TimerEntryFields.isActive: isActice ? 1 : 0,
TimerEntryFields.stopWatch: stopwatch!.elapsed.inMilliseconds,
};
}
}

@ -40,8 +40,11 @@ class AboutScreen extends StatelessWidget {
textAlign: TextAlign.justify,
),
//
applicationLegalese:
"Copyright © 2023 中国民航大学 计算机科学与技术学院 \n 计算机科学与技术专业 21034102班 \n 庞浩,葛兴海,蔡玉祥,邹兴云,卫俊钢 小组",
applicationLegalese: '''
Copyright © 2023
21034102
''',
/**
* ListTile
*/

@ -1,316 +1,13 @@
// import 'package:flutter/cupertino.dart';
// import 'package:flutter/material.dart';
// class SpaceWidget extends StatelessWidget {
// final double width;
// final double height;
// SpaceWidget({this.width = 0.0, this.height = 0.0});
// @override
// Widget build(BuildContext context) {
// return SizedBox(
// width: width,
// height: height,
// );
// }
// }
// class SyllabusPage extends StatefulWidget {
// @override
// State<StatefulWidget> createState() => PageState();
// }
// class PageState extends State<SyllabusPage> {
// var colorList = [
// Colors.red,
// Colors.lightBlueAccent,
// Colors.grey,
// Colors.cyan,
// Colors.amber,
// Colors.deepPurpleAccent,
// Colors.purpleAccent
// ];
// var infoList = ["高等数学-周某某教授@综合楼201", "大学英语-王某某讲师@行政楼501"];
// var weekList = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
// var dateList = [];
// var currentWeekIndex = 0;
// @override
// void initState() {
// super.initState();
// var monday = 1;
// var mondayTime = DateTime.now();
// //
// while (mondayTime.weekday != monday) {
// mondayTime = mondayTime.subtract(new Duration(days: 1));
// }
// mondayTime.year; //2020
// mondayTime.month; //6(jsjs0dart1)
// mondayTime.day; //6
// // nowTime.hour ;//6
// // nowTime.minute ;//6
// // nowTime.second ;//6
// for (int i = 0; i < 7; i++) {
// dateList.add(
// mondayTime.month.toString() + "/" + (mondayTime.day + i).toString());
// if ((mondayTime.day + i) == DateTime.now().day) {
// setState(() {
// currentWeekIndex = i + 1;
// });
// }
// }
// // print('Recent monday '+DateTime.now().day.toString());
// }
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// body: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// SizedBox(
// child: GridView.builder(
// shrinkWrap: true,
// physics: NeverScrollableScrollPhysics(),
// itemCount: 8,
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 8, childAspectRatio: 1 / 1),
// itemBuilder: (BuildContext context, int index) {
// return Container(
// color: index == this.currentWeekIndex
// ? Color(0xf7f7f7)
// : Colors.white,
// child: Center(
// child: index == 0
// ? Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text("星期",
// style: TextStyle(
// fontSize: 14, color: Colors.black87)),
// SpaceWidget(height: 5),
// Text("日期", style: TextStyle(fontSize: 12)),
// ],
// )
// : Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(weekList[index - 1],
// style: TextStyle(
// fontSize: 14,
// color: index == currentWeekIndex
// ? Colors.lightBlue
// : Colors.black87)),
// SpaceWidget(height: 5),
// Text(dateList[index - 1],
// style: TextStyle(
// fontSize: 12,
// color: index == currentWeekIndex
// ? Colors.lightBlue
// : Colors.black87)),
// ],
// ),
// ),
// );
// }),
// ),
// Expanded(
// child: SingleChildScrollView(
// child: Row(
// children: [
// Expanded(
// flex: 1,
// child: GridView.builder(
// shrinkWrap: true,
// // physics:ClampingScrollPhysics(),
// itemCount: 10,
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 1, childAspectRatio: 1 / 2),
// itemBuilder: (BuildContext context, int index) {
// return Container(
// // width: 25,
// // height:s 80,
// child: Center(
// child: Text(
// (index + 1).toInt().toString(),
// style: TextStyle(fontSize: 15),
// ),
// ),
// decoration: BoxDecoration(
// color: Color(0xff5ff5),
// // border: Border.all(color: Colors.black12, width: 0.5),
// border: Border(
// bottom: BorderSide(
// color: Colors.black12, width: 0.5),
// right: BorderSide(
// color: Colors.black12, width: 0.5),
// ),
// ));
// }),
// ),
// Expanded(
// flex: 7,
// child: GridView.builder(
// shrinkWrap: true,
// physics: NeverScrollableScrollPhysics(),
// itemCount: 35,
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 7, childAspectRatio: 1 / 4),
// itemBuilder: (BuildContext context, int index) {
// return Container(
// child: Stack(
// children: [
// Column(
// mainAxisAlignment:
// MainAxisAlignment.spaceBetween,
// children: [
// Flexible(
// flex: 1,
// child: Container(
// width: double.infinity,
// height: double.infinity,
// decoration: BoxDecoration(
// color: Colors.white,
// // border: Border.all(color: Colors.black12, width: 0.5),
// border: Border(
// bottom: BorderSide(
// color: Colors.black12,
// width: 0.5),
// right: BorderSide(
// color: Colors.black12,
// width: 0.5),
// ),
// )),
// ),
// Flexible(
// flex: 1,
// child: Container(
// width: double.infinity,
// height: double.infinity,
// decoration: BoxDecoration(
// color: Colors.white,
// // border: Border.all(color: Colors.black12, width: 0.5),
// border: Border(
// bottom: BorderSide(
// color: Colors.black12,
// width: 0.5),
// right: BorderSide(
// color: Colors.black12,
// width: 0.5),
// ),
// )),
// ),
// ],
// ),
// if (index % 5 == 0 || index % 5 == 1)
// Container(
// margin: EdgeInsets.all(0.5),
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(2),
// color: colorList[index % 7],
// ),
// child: Center(
// child: Text(
// infoList[index % 2],
// textAlign: TextAlign.center,
// style: TextStyle(
// color: Colors.white,
// fontSize: 11,
// letterSpacing: 1),
// ),
// ),
// )
// ],
// ),
// );
// }),
// )
// ],
// ),
// ),
// ),
// _bottomView
// ],
// ),
// );
// }
// @override
// String pageTitle() => "我的课表";
// Widget _topView = SizedBox(
// height: 30,
// child: Expanded(
// child: ListView.builder(
// scrollDirection: Axis.horizontal,
// itemCount: 7,
// itemBuilder: (BuildContext context, int index) {
// return Text("dd");
// }),
// ),
// );
// Widget _centerView = Expanded(
// child: GridView.builder(
// itemCount: 63,
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 7,
// ),
// itemBuilder: (BuildContext context, int index) {
// return Container(
// // width: 25,
// // height: 80,
// child: Center(
// child: Text(
// (index + 1).toString(),
// style: TextStyle(fontSize: 15),
// ),
// ),
// decoration: BoxDecoration(
// color: Color(0xff5ff5),
// border: Border.all(color: Colors.black12, width: 0.5),
// ));
// }),
// );
// Widget _bottomView = SizedBox(
// height: 30,
// child: Row(
// children: [
// //view
// ],
// ),
// );
// }
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SpaceWidget extends StatelessWidget {
final double width;
final double height;
SpaceWidget({this.width = 0.0, this.height = 0.0});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
);
}
}
class CourseScreen extends StatefulWidget {
const CourseScreen({super.key});
class SyllabusPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => PageState();
State<StatefulWidget> createState() => CourseScreenState();
}
class PageState extends State<SyllabusPage> {
class CourseScreenState extends State<CourseScreen> {
var colorList = [
Colors.red,
Colors.lightBlueAccent,
@ -335,25 +32,13 @@ class PageState extends State<SyllabusPage> {
//
while (mondayTime.weekday != monday) {
mondayTime = mondayTime.subtract(new Duration(days: 1));
mondayTime = mondayTime.subtract(Duration(days: 1));
}
mondayTime.year; //2020
mondayTime.month; //6(jsjs0dart1)
mondayTime.day; //6
// nowTime.hour ;//6
// nowTime.minute ;//6
// nowTime.second ;//6
for (int i = 0; i < 7; i++) {
dateList.add(
mondayTime.month.toString() + "/" + (mondayTime.day + i).toString());
if ((mondayTime.day + i) == DateTime.now().day) {
setState(() {
currentWeekIndex = i + 1;
});
}
dateList.add("${mondayTime.month}-${mondayTime.day}");
mondayTime = mondayTime.add(Duration(days: 1));
}
// print('Recent monday '+DateTime.now().day.toString());
}
@override
@ -371,18 +56,18 @@ class PageState extends State<SyllabusPage> {
crossAxisCount: 8, childAspectRatio: 1 / 1),
itemBuilder: (BuildContext context, int index) {
return Container(
color: index == this.currentWeekIndex
? Color(0xf7f7f7)
color: index == currentWeekIndex
? Color(0x00f7f7f7)
: Colors.white,
child: Center(
child: index == 0
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: const [
Text("星期",
style: TextStyle(
fontSize: 14, color: Colors.black87)),
SpaceWidget(height: 5),
SizedBox(height: 5),
Text("日期", style: TextStyle(fontSize: 12)),
],
)
@ -395,7 +80,7 @@ class PageState extends State<SyllabusPage> {
color: index == currentWeekIndex
? Colors.lightBlue
: Colors.black87)),
SpaceWidget(height: 5),
SizedBox(height: 5),
Text(dateList[index - 1],
style: TextStyle(
fontSize: 12,
@ -422,16 +107,8 @@ class PageState extends State<SyllabusPage> {
crossAxisCount: 1, childAspectRatio: 1 / 2),
itemBuilder: (BuildContext context, int index) {
return Container(
// width: 25,
// height:s 80,
child: Center(
child: Text(
(index + 1).toInt().toString(),
style: TextStyle(fontSize: 15),
),
),
decoration: BoxDecoration(
color: Color(0xff5ff5),
color: Color(0x00ff5ff5),
// border: Border.all(color: Colors.black12, width: 0.5),
border: Border(
bottom: BorderSide(
@ -439,6 +116,14 @@ class PageState extends State<SyllabusPage> {
right: BorderSide(
color: Colors.black12, width: 0.5),
),
),
// width: 25,
// height:s 80,
child: Center(
child: Text(
(index + 1).toInt().toString(),
style: TextStyle(fontSize: 15),
),
));
}),
),
@ -483,69 +168,67 @@ class PageState extends State<SyllabusPage> {
);
},
child: Container(
child: Stack(
children: [
Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(
color: Colors.black12,
width: 0.5),
right: BorderSide(
color: Colors.black12,
width: 0.5),
),
)),
),
Flexible(
flex: 1,
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(
color: Colors.black12,
width: 0.5),
right: BorderSide(
color: Colors.black12,
width: 0.5),
),
)),
),
],
),
if (index % 5 == 0 || index % 5 == 1)
Container(
margin: EdgeInsets.all(0.5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: colorList[index % 7],
),
child: Center(
child: Text(
infoList[index % 2],
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 11,
letterSpacing: 1),
),
child: Stack(
children: [
Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(
color: Colors.black12,
width: 0.5),
right: BorderSide(
color: Colors.black12,
width: 0.5),
),
)),
),
Flexible(
flex: 1,
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(
color: Colors.black12,
width: 0.5),
right: BorderSide(
color: Colors.black12,
width: 0.5),
),
)),
),
],
),
if (index % 5 == 0 || index % 5 == 1)
Container(
margin: EdgeInsets.all(0.5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: colorList[index % 7],
),
child: Center(
child: Text(
infoList[index % 2],
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 11,
letterSpacing: 1),
),
)
],
),
),
)
],
),
);
}),
@ -560,77 +243,28 @@ class PageState extends State<SyllabusPage> {
);
}
@override
String pageTitle() => "我的课表";
Widget _topView = SizedBox(
height: 30,
child: Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 7,
itemBuilder: (BuildContext context, int index) {
return Text("dd");
}),
),
);
Widget _centerView = Expanded(
child: GridView.builder(
itemCount: 63,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
),
itemBuilder: (BuildContext context, int index) {
return Container(
// width: 25,
// height: 80,
child: Center(
child: Text(
(index + 1).toString(),
style: TextStyle(fontSize: 15),
),
),
decoration: BoxDecoration(
color: Color(0xff5ff5),
border: Border.all(color: Colors.black12, width: 0.5),
));
}),
);
Widget _bottomView = SizedBox(
final Widget _bottomView = SizedBox(
height: 30,
child: Row(
children: [
children: const [
//view
],
),
);
}
// class EditCoursePage extends StatelessWidget {
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: Text('编辑课程'),
// ),
// body: Center(
// child: Text('在这里编辑课程信息'),
// ),
// );
// }
// }
class EditCoursePage extends StatefulWidget {
final Function(String) onSave;
EditCoursePage({required this.onSave});
const EditCoursePage({super.key, required this.onSave});
@override
_EditCoursePageState createState() => _EditCoursePageState();
EditCoursePageState createState() => EditCoursePageState();
}
class _EditCoursePageState extends State<EditCoursePage> {
class EditCoursePageState extends State<EditCoursePage> {
final _formKey = GlobalKey<FormState>();
final _courseController = TextEditingController();

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:timemanage/screen/reports_screen.dart';
import 'package:timemanage/screen/export_screen.dart';
import 'package:timemanage/screen/settings_screen.dart';
import 'package:timemanage/screen/about_screen.dart';
import 'package:timemanage/screen/course_screen.dart';
class HomeMenuBar extends StatefulWidget {
const HomeMenuBar({Key? key}) : super(key: key);
@override
HomeMenuBarState createState() => HomeMenuBarState();
}
class HomeMenuBarState extends State<HomeMenuBar> {
@override
Widget build(BuildContext context) {
return MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CourseScreen()),
);
},
child: const Text('课程表'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ReportsScreen()),
);
},
child: const Text('统计报告'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ExportScreen()),
);
},
child: const Text('导入和导出'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen()),
);
},
child: const Text('设置'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AboutScreen()),
);
},
child: const Text('关于'),
),
],
child: const Icon(Icons.menu),
),
],
);
}
}

@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import 'package:timemanage/db/timer_entry_database.dart';
import 'package:timemanage/model/timer_entry.dart';
import 'package:timemanage/screen/dashboard/components/home_menu_bar.dart';
//
class DashBoardScreen extends StatefulWidget {
const DashBoardScreen({Key? key}) : super(key: key);
@override
DashBoardScreenState createState() => DashBoardScreenState();
}
class DashBoardScreenState extends State<DashBoardScreen> {
late List<TimerEntry> timers; //
bool isLoading = false; //
//
@override
void initState() {
super.initState();
refreshTimers();
}
//
@override
void dispose() {
TimerEntryDatabase.instance.close();
super.dispose();
}
//
Future refreshTimers() async {
setState(() {
isLoading = true;
});
timers = await TimerEntryDatabase.instance.readAll();
setState(() {
timers = timers;
isLoading = false;
});
}
//
void _addTimer() {
final controller = TextEditingController();
showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('新建计时器'),
//
content: SizedBox(
height: 100,
child: Column(
children: [
TextField(
decoration: InputDecoration(hintText: '计时器名称'),
controller: controller,
),
],
),
),
// or
actions: [
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('取消'),
),
TextButton(
onPressed: () {
final name = controller.text;
// TODO:
if (name.isNotEmpty) {
TimerEntry timerEntry = TimerEntry(
name: name,
);
setState(() {
timers.add(timerEntry);
});
//
TimerEntryDatabase.instance.create(timerEntry);
}
Navigator.pop(context, 'OK');
},
child: const Text('确定'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
//
appBar: AppBar(
backgroundColor: Colors.blueAccent, //
//
leading: HomeMenuBar(),
//
title: const Text('时间管理'),
actions: [
//
// TODO:
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
// //
// IconButton(
// icon: const Icon(Icons.filter_alt),
// onPressed: () {},
// )
],
),
//
body: isLoading
? const Center(child: CircularProgressIndicator())
: timers.isEmpty
? const Center(child: Text('还没有计时器,先添加一个吧!'))
: buildTimers(),
//
floatingActionButton: FloatingActionButton(
onPressed: _addTimer,
child: const Icon(Icons.add),
),
);
}
//
Widget buildTimers() {
return ListView.builder(
itemCount: timers.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
leading: CircleAvatar(
child: Text((index + 1).toString()),
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
timers[index].createdAt.toString().substring(0, 16),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
timers[index].name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
StreamBuilder<int>(
stream: Stream.periodic(const Duration(milliseconds: 100),
(_) => timers[index].stopwatch!.elapsed.inSeconds),
builder: (context, snapshot) {
final seconds = snapshot.data ?? 0;
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
final remainingSeconds = seconds % 60;
return Text(
'${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: timers[index].isActive
? const Icon(Icons.pause)
: const Icon(Icons.play_arrow),
onPressed: () {
setState(() {
if (timers[index].isActive) {
timers[index].stop();
} else {
timers[index].start();
}
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
timers[index].stopwatch!.stop(); //
timers.removeAt(index);
});
},
),
],
),
),
const SizedBox(height: 10), //
],
);
},
);
}
}

@ -1,244 +0,0 @@
import 'package:flutter/material.dart';
import 'package:timemanage/screen/projects_screen.dart';
import 'package:timemanage/screen/reports_screen.dart';
import 'package:timemanage/screen/export_screen.dart';
import 'package:timemanage/screen/settings_screen.dart';
import 'package:timemanage/screen/about_screen.dart';
import 'package:timemanage/screen/course_screen.dart';
class DashBoardScreen extends StatefulWidget {
const DashBoardScreen({Key? key}) : super(key: key);
@override
_DashBoardScreenState createState() => _DashBoardScreenState();
}
class _DashBoardScreenState extends State<DashBoardScreen> {
List<TimerModel> timers = [];
void _addTimer() {
final controller = TextEditingController();
showDialog<String>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('新计时器'),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: '计时器名称'),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('取消'),
),
TextButton(
onPressed: () {
final name = controller.text;
if (name.isNotEmpty) {
setState(() {
timers.add(TimerModel(name: name));
});
}
Navigator.pop(context, 'OK');
},
child: const Text('OK'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blueAccent, //
//
leading: MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SyllabusPage()),
);
},
child: const Text('课程表'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProjectsScreen()),
);
},
child: const Text('项目'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ReportsScreen()),
);
},
child: const Text('统计报告'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ExportScreen()),
);
},
child: const Text('导入和导出'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen()),
);
},
child: const Text('设置'),
),
MenuItemButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AboutScreen()),
);
},
child: const Text('关于'),
),
],
child: const Icon(Icons.menu),
),
],
),
//
title: const Text('时间管理'),
actions: [
//
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
//
IconButton(
icon: const Icon(Icons.add),
onPressed: _addTimer,
),
IconButton(
icon: const Icon(Icons.filter_alt),
onPressed: () {},
)
],
),
body: ListView.builder(
itemCount: timers.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
leading: CircleAvatar(
child: Text((index + 1).toString()),
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
timers[index].name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
StreamBuilder<int>(
stream: Stream.periodic(const Duration(milliseconds: 100),
(_) => timers[index].stopwatch.elapsed.inSeconds),
builder: (context, snapshot) {
final seconds = snapshot.data ?? 0;
final hours = seconds ~/ 3600;
final minutes = (seconds % 3600) ~/ 60;
final remainingSeconds = seconds % 60;
return Text(
'${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: timers[index].isActive
? const Icon(Icons.pause)
: const Icon(Icons.play_arrow),
onPressed: () {
setState(() {
if (timers[index].isActive) {
timers[index].stop();
} else {
timers[index].start();
}
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
timers[index].stopwatch.stop(); //
timers.removeAt(index);
});
},
),
],
),
),
const SizedBox(height: 10), //
],
);
},
),
);
}
}
class TimerModel {
String name;
Stopwatch stopwatch;
bool isActive;
TimerModel({required this.name})
: stopwatch = Stopwatch(),
isActive = false;
void start() {
stopwatch.start();
isActive = true;
}
void stop() {
stopwatch.stop();
isActive = false;
}
void reset() {
stopwatch.reset();
}
}

@ -1,16 +0,0 @@
import 'package:flutter/material.dart';
class ProjectsScreen extends StatelessWidget {
const ProjectsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('项目'),
),
body: const Center(
child: Text('项目界面'),
),
);
}
}

@ -1,15 +1,109 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:timemanage/db/timer_entry_database.dart';
class ReportsScreen extends StatelessWidget {
class ReportsScreen extends StatefulWidget {
const ReportsScreen({super.key});
@override
State<ReportsScreen> createState() => _ReportsScreenState();
}
//
class _ReportsScreenState extends State<ReportsScreen> {
//
var timers = TimerEntryDatabase.instance.readAll();
Map<String, num> persentMap = {};
@override
void initState() {
super.initState();
classifyTimers().then((value) {
setState(() {
persentMap = value;
});
});
for (var key in persentMap.keys) {
log(key);
}
}
Future<Map<String, num>> classifyTimers() async {
var timers = await TimerEntryDatabase.instance.readAll();
Map<String, num> persentMap = {};
for (var timer in timers) {
var response = await postData(timer.name);
var classification =
jsonDecode(response.body)['item']['lv1_tag_list'][0]['tag'];
// APIISO-8859-1
classification = utf8.decode(classification.runes.toList());
persentMap[classification] = timer.stopWatch;
log('classification: $classification');
}
return persentMap;
}
// API
Future<http.Response> postData(String name) async {
// ignore: non_constant_identifier_names
var API_KEY = "YGxSMNdWKO5Gpt12dNVY2nGq";
// ignore: non_constant_identifier_names
var SECRET_KEY = "G5NBDTiRPhsBqMi1Od0XZw4RSMlFgeek";
var accessToken = await getAccessToken(API_KEY, SECRET_KEY);
var url = Uri.parse(
'https://aip.baidubce.com/rpc/2.0/nlp/v1/topic?charset=UTF-8&access_token=$accessToken');
var payload = jsonEncode({
"title": name,
"content": name,
});
var headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
var response = await http.post(
url,
headers: headers,
body: payload,
);
return response;
}
// ignore: non_constant_identifier_names
Future<String> getAccessToken(String API_KEY, String SECRET_KEY) async {
var url = 'https://aip.baidubce.com/oauth/2.0/token';
var params = {
'grant_type': 'client_credentials',
'client_id': API_KEY,
'client_secret': SECRET_KEY,
};
var response = await http.post(
Uri.parse(url),
body: params,
);
if (response.statusCode == 200) {
var data = jsonDecode(response.body);
return data['access_token'];
} else {
throw Exception('Failed to get access token');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('统计报告'),
),
body: const Center(
child: Text('统计报告界面'),
body: ListView.builder(
itemCount: persentMap.length,
itemBuilder: (context, index) => ListTile(
title: Text(persentMap.keys.toList()[index]),
subtitle: Text(persentMap.values.toList()[index].toString()),
),
),
);
}

@ -6,12 +6,14 @@ import FlutterMacOS
import Foundation
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import sqflite
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

@ -33,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: "direct main"
description:
name: bloc
sha256: "6f1b87b6eca9041d5672b6e29273cd1594db48ebb66fd2471066e9f3c3a516bd"
url: "https://pub.dev"
source: hosted
version: "7.2.1"
boolean_selector:
dependency: transitive
description:
@ -134,6 +142,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.1"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: cdd1351ced09eeb46cfa7946e095b7679344af927415ca9cd972928fa6d5b23f
url: "https://pub.dev"
source: hosted
version: "7.3.3"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -185,13 +201,13 @@ packages:
source: hosted
version: "9.2.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "0.13.6"
http_parser:
dependency: transitive
description:
@ -208,6 +224,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0"
intl:
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:
@ -264,6 +288,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_info_plus:
dependency: "direct main"
description:
@ -281,7 +313,7 @@ packages:
source: hosted
version: "2.0.1"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@ -296,6 +328,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_linux:
dependency: transitive
description:
@ -352,6 +408,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.7.3"
provider:
dependency: transitive
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
url: "https://pub.dev"
source: hosted
version: "6.0.5"
shared_preferences:
dependency: "direct main"
description:

@ -37,13 +37,19 @@ 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
flutter_bloc: ^7.0.0
bloc: ^7.0.0
path: ^1.8.0
shared_preferences: ^2.0.8
sqflite: ^2.0.0+3 # 用于数据库
intl: ^0.17.0 # 用于时间格式化
http: ^0.13.3 # 用于网络请求
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
path_provider: ^2.1.1
dev_dependencies:
flutter_test:

@ -5,26 +5,6 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:timemanage/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
print('Hello World!');
}

Loading…
Cancel
Save