Compare commits
26
Commits
be43a1d6e9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ececb7439a | ||
|
|
fd171b8aa7 | ||
|
|
e01bd9895e | ||
|
|
d7ecab3415 | ||
|
|
7b8cb39567 | ||
|
|
8161b23755 | ||
|
|
a7830aec27 | ||
|
|
b110ea0cb9 | ||
|
|
03382d2cb9 | ||
|
|
315a113f16 | ||
|
|
9824429703 | ||
|
|
2195a5324c | ||
|
|
bbbe05ed3e | ||
|
|
e18abcdcb8 | ||
|
|
5eb90ac1e1 | ||
|
|
66028b831d | ||
|
|
0ea14e9c02 | ||
|
|
ca0fe7dd61 | ||
|
|
dfeb19126e | ||
|
|
4f7d3879a8 | ||
|
|
5dcfd5da8d | ||
|
|
70aa37f3f5 | ||
|
|
c9c3f69886 | ||
|
|
0dd56cfaa6 | ||
|
|
162f2469f8 | ||
|
|
77e4adc2ad |
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ltx_flutter",
|
||||
"cwd": "ltx_flutter",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "ltx_flutter (profile mode)",
|
||||
"cwd": "ltx_flutter",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "ltx_flutter (release mode)",
|
||||
"cwd": "ltx_flutter",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "flutter",
|
||||
"command": "flutter",
|
||||
"args": [
|
||||
"build",
|
||||
"apk",
|
||||
"--split-per-abi"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "flutter: flutter build apk",
|
||||
"detail": "ltx_flutter"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
flake-profile-2-link
|
||||
@@ -0,0 +1 @@
|
||||
/nix/store/548c9rk983kzvkfiicqmkslqmd7s1ldg-nix-shell-env
|
||||
@@ -0,0 +1 @@
|
||||
/nix/store/rmm84806g3pgv1nc8wvi7g72ivh6axsi-nix-shell-env
|
||||
@@ -48,7 +48,7 @@ android {
|
||||
applicationId "com.ryanpandya.lifetracker"
|
||||
// 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
|
||||
minSdkVersion 19 //flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:label="ltx_flutter"
|
||||
android:label="Lifetracker"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<!-- Add this inside the `<application>` tag, along side the existing `<activity>` tags -->
|
||||
|
||||
Generated
+168
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"nodes": {
|
||||
"android-nixpkgs": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1695500385,
|
||||
"narHash": "sha256-bpTF7H5fjfZMwgU8/W28FPgkyTf2rYTVUMBgmMskk44=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "a85aaf82d9c29b41e37bc0babd3f69632dfdbaf1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"android-nixpkgs",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1695195896,
|
||||
"narHash": "sha256-pq9q7YsGXnQzJFkR5284TmxrLNFc0wo4NQ/a5E93CQU=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "05d40d17bf3459606316e3e9ec683b784ff28f16",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1695145219,
|
||||
"narHash": "sha256-Eoe9IHbvmo5wEDeJXKFOpKUwxYJIOxKUesounVccNYk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5ba549eafcf3e33405e5f66decd1a72356632b96",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1685566663,
|
||||
"narHash": "sha256-btHN1czJ6rzteeCuE/PNrdssqYD2nIA4w48miQAFloM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4ecab3273592f27479a583fb6d975d4aba3486fe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "23.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"android-nixpkgs": "android-nixpkgs",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
description = "moxplatform";
|
||||
inputs = {
|
||||
# nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/23.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, android-nixpkgs }: flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
android_sdk.accept_license = true;
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
# Everything to make Flutter happy
|
||||
sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
|
||||
cmdline-tools-latest
|
||||
build-tools-30-0-3
|
||||
build-tools-33-0-2
|
||||
build-tools-34-0-0
|
||||
platform-tools
|
||||
emulator
|
||||
patcher-v4
|
||||
platforms-android-30
|
||||
platforms-android-31
|
||||
platforms-android-33
|
||||
]);
|
||||
pinnedJDK = pkgs.jdk17;
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
bashInteractive
|
||||
|
||||
# Android
|
||||
pinnedJDK
|
||||
sdk
|
||||
|
||||
# Flutter
|
||||
flutter dart
|
||||
|
||||
# Code hygiene
|
||||
gitlint
|
||||
|
||||
# Flutter dependencies for linux desktop
|
||||
atk
|
||||
cairo
|
||||
clang
|
||||
cmake
|
||||
epoxy
|
||||
gdk-pixbuf
|
||||
glib
|
||||
gtk3
|
||||
harfbuzz
|
||||
ninja
|
||||
pango
|
||||
pcre
|
||||
pkg-config
|
||||
xorg.libX11
|
||||
xorg.xorgproto
|
||||
];
|
||||
|
||||
# Make Flutter build on desktop
|
||||
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";
|
||||
LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ atk cairo epoxy gdk-pixbuf glib gtk3 harfbuzz pango ];
|
||||
|
||||
ANDROID_HOME = "${sdk}/share/android-sdk";
|
||||
ANDROID_SDK_ROOT = "${sdk}/share/android-sdk";
|
||||
JAVA_HOME = pinnedJDK;
|
||||
|
||||
# Fix an issue with Flutter using an older version of aapt2, which does not know
|
||||
# an used parameter.
|
||||
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${sdk}/share/android-sdk/build-tools/34.0.0/aapt2";
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'dart:async';
|
||||
|
||||
final appBloc = AppPropertiesBloc();
|
||||
|
||||
class AppPropertiesBloc {
|
||||
StreamController<String> _title = StreamController<String>();
|
||||
StreamController<bool> _editable = StreamController<bool>();
|
||||
|
||||
Stream<String> get titleStream => _title.stream;
|
||||
Stream<bool> get editable => _editable.stream;
|
||||
|
||||
updateTitle(String newTitle) {
|
||||
_title.sink.add(newTitle);
|
||||
}
|
||||
|
||||
toggleEditable(bool editable) {
|
||||
_editable.sink.add(!editable);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_title.close();
|
||||
_editable.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import 'package:appwrite/appwrite.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:ltx_flutter/constants/constants.dart';
|
||||
import 'package:ltx_flutter/constants/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CategoryData {
|
||||
late String? color;
|
||||
late num? number;
|
||||
late num? parent;
|
||||
late String? name;
|
||||
late String? description;
|
||||
|
||||
CategoryData({
|
||||
this.color,
|
||||
this.number,
|
||||
this.parent,
|
||||
this.name,
|
||||
this.description,
|
||||
}) {
|
||||
color = color;
|
||||
number = number;
|
||||
parent = parent;
|
||||
name = name;
|
||||
description = description;
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
late final Color backgroundColor;
|
||||
late final Color foregroundColor;
|
||||
late final num number;
|
||||
late final num? parent;
|
||||
late final String name;
|
||||
late final String? description;
|
||||
late final String hexColor;
|
||||
|
||||
Category(Document doc) {
|
||||
hexColor = doc.data['color'];
|
||||
backgroundColor = _getBackgroundColor(doc.data['color']);
|
||||
foregroundColor = _getForegroundColor(doc.data['color']);
|
||||
number = doc.data['number'];
|
||||
parent = doc.data['parent'];
|
||||
name = doc.data['name'];
|
||||
description = doc.data['description'];
|
||||
}
|
||||
|
||||
Color _getBackgroundColor(String colorStr) {
|
||||
return Color(int.parse("0xff$colorStr"));
|
||||
}
|
||||
|
||||
Color _getForegroundColor(String colorStr) {
|
||||
return Color(int.parse("0xff$colorStr")).computeLuminance() > 0.2
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
}
|
||||
|
||||
bool hasParent() {
|
||||
return parent != number;
|
||||
}
|
||||
|
||||
double leftPadding() {
|
||||
return hasParent() ? 30.0 : 15.0;
|
||||
}
|
||||
|
||||
CategoryData data() {
|
||||
return CategoryData(
|
||||
description: description,
|
||||
color: hexColor,
|
||||
name: name,
|
||||
number: number,
|
||||
parent: parent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoriesAPI extends ChangeNotifier {
|
||||
Client client = Client();
|
||||
late final Account account;
|
||||
late final Databases databases;
|
||||
late List<Document> _categories = [];
|
||||
bool _ready = false;
|
||||
|
||||
// Constructor
|
||||
CategoriesAPI() {
|
||||
init();
|
||||
getCategories();
|
||||
}
|
||||
|
||||
// Getters
|
||||
get total => _categories.length;
|
||||
get all => _categories;
|
||||
get isEmpty => _categories.isEmpty;
|
||||
get(n) => Category(_categories[n]);
|
||||
get colors => CategoryColor.values;
|
||||
get ready => _ready;
|
||||
|
||||
lookUp(n) {
|
||||
if (!_ready) {
|
||||
return false;
|
||||
}
|
||||
try{
|
||||
return Category(_categories.singleWhere((e) {
|
||||
return e.data['number'] == double.parse(n.toString());
|
||||
}));
|
||||
}
|
||||
catch(e){
|
||||
return Category(_categories.singleWhere((e) {
|
||||
return e.data['number'] == -9;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID);
|
||||
account = Account(client);
|
||||
databases = Databases(client);
|
||||
}
|
||||
|
||||
getCategories() async {
|
||||
print("Updating categories.");
|
||||
_categories = [];
|
||||
var response = await databases.listDocuments(
|
||||
databaseId: CATEGORIES_DB,
|
||||
collectionId: COLLECTION,
|
||||
queries: [
|
||||
Query.limit(100),
|
||||
Query.orderAsc("parent"),
|
||||
Query.orderAsc("number"),
|
||||
]);
|
||||
_categories = response.documents;
|
||||
notifyListeners();
|
||||
_ready = true;
|
||||
}
|
||||
|
||||
Future<Document>? addCategory({
|
||||
required String name,
|
||||
required double number,
|
||||
required String color,
|
||||
String? description,
|
||||
int? parentId,
|
||||
}) async {
|
||||
try {
|
||||
return await databases.createDocument(
|
||||
databaseId: CATEGORIES_DB,
|
||||
collectionId: COLLECTION,
|
||||
documentId: "category-${number.toString()}",
|
||||
data: {
|
||||
'name': name,
|
||||
'number': number,
|
||||
'color': color,
|
||||
'parent': parentId,
|
||||
'description': description,
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
throw "Didn't work";
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Document>? updateCategory({
|
||||
required String name,
|
||||
required double number,
|
||||
required String color,
|
||||
String? description,
|
||||
int? parentId,
|
||||
}) async {
|
||||
try {
|
||||
print("Updating '${"category-${number.toString()}"}'");
|
||||
var x = await databases.updateDocument(
|
||||
databaseId: CATEGORIES_DB,
|
||||
collectionId: COLLECTION,
|
||||
documentId: "category-${number.toString()}",
|
||||
data: {
|
||||
'name': name,
|
||||
'number': number,
|
||||
'color': color,
|
||||
'parent': parentId,
|
||||
'description': description,
|
||||
});
|
||||
return x;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
throw "Didn't work";
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> deleteCategory({required double number}) {
|
||||
return databases.deleteDocument(
|
||||
databaseId: CATEGORIES_DB,
|
||||
collectionId: COLLECTION,
|
||||
documentId: "category-${number.toString()}");
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,231 @@
|
||||
import 'package:appwrite/appwrite.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:ltx_flutter/appwrite/appwrite.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:ltx_flutter/constants/constants.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DatabaseAPI {
|
||||
class DatabaseAPI extends ChangeNotifier {
|
||||
Client client = Client();
|
||||
late final Account account;
|
||||
late final Databases databases;
|
||||
late final Realtime realtime;
|
||||
final AuthAPI auth = AuthAPI();
|
||||
|
||||
late List<Document> _entries = [];
|
||||
bool _ready = false;
|
||||
|
||||
// Getter methods
|
||||
List<Document> get entries => _entries;
|
||||
int get length => _entries.length;
|
||||
bool get ready => _ready;
|
||||
double get progress => length / total();
|
||||
|
||||
final DateFormat formatter = DateFormat('yyyy-MM-dd');
|
||||
|
||||
// Constructor
|
||||
DatabaseAPI() {
|
||||
init();
|
||||
getAll();
|
||||
subscribeRealtime();
|
||||
}
|
||||
|
||||
init() {
|
||||
client
|
||||
.setEndpoint(APPWRITE_URL)
|
||||
.setProject(APPWRITE_PROJECT_ID)
|
||||
.setSelfSigned();
|
||||
client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID);
|
||||
account = Account(client);
|
||||
databases = Databases(client);
|
||||
realtime = Realtime(client);
|
||||
}
|
||||
|
||||
Future<DocumentList> getEntries({int limit = 100, String dateISO = ""}) {
|
||||
if (dateISO == "") {
|
||||
dateISO = formatter.format(DateTime.now());
|
||||
}
|
||||
elDate(dynamic el) {
|
||||
return formatter.format(DateTime.parse(el.data['date']));
|
||||
}
|
||||
|
||||
sortByDate(entries) {
|
||||
return entries.sort(
|
||||
(a, b) => a.$id.compareTo(b.$id),
|
||||
);
|
||||
}
|
||||
|
||||
subscribeRealtime() {
|
||||
final subscription = realtime.subscribe(
|
||||
['databases.$APPWRITE_DATABASE_ID.collections.$COLLECTION.documents']);
|
||||
|
||||
print("Subscribed to realtime");
|
||||
subscription.stream.listen((response) {
|
||||
String dateISO =
|
||||
formatter.format(DateTime.parse(response.payload['date']));
|
||||
if (response.events.contains(
|
||||
'databases.$APPWRITE_DATABASE_ID.collections.$COLLECTION.documents.*.update')) {
|
||||
// Entry was updated
|
||||
|
||||
int entryIndex =
|
||||
_entries.indexWhere((element) => elDate(element) == dateISO);
|
||||
Document newEntry = _entries[entryIndex];
|
||||
newEntry.data['hours'] = response.payload['hours'];
|
||||
newEntry.data['mood'] = response.payload['mood'];
|
||||
newEntry.data['comments'] = response.payload['comments'];
|
||||
|
||||
_entries.removeAt(entryIndex);
|
||||
_entries.add(newEntry);
|
||||
notifyListeners();
|
||||
// _entries.add(entry);
|
||||
// sortByDate(_entries);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int total() {
|
||||
String dateISO = DateTime.now().toIso8601String();
|
||||
|
||||
var referenceDate = DateTime.parse("2023-01-01");
|
||||
|
||||
final date = DateTime.parse(dateISO);
|
||||
final offset = date.difference(DateTime.now()).inDays;
|
||||
|
||||
print("Getting ${limit} entries starting from ${offset}");
|
||||
int _total = date.difference(referenceDate).inDays + 1;
|
||||
return _total;
|
||||
}
|
||||
|
||||
return databases.listDocuments(
|
||||
getAll() async {
|
||||
print("Loading entries");
|
||||
|
||||
String paginationQuery = Query.offset(0);
|
||||
int max = total();
|
||||
|
||||
while (_entries.length < max) {
|
||||
int remainder = max - _entries.length;
|
||||
int limit = remainder > 100 ? 100 : remainder;
|
||||
|
||||
if (_entries.isNotEmpty) {
|
||||
String lastDate = _entries.last.$id;
|
||||
paginationQuery = Query.cursorAfter(lastDate);
|
||||
}
|
||||
|
||||
List<Document> newEntries = await getEntries(
|
||||
limit: limit,
|
||||
paginationQuery: paginationQuery,
|
||||
);
|
||||
|
||||
if(newEntries.isEmpty){
|
||||
// This only happens if we were expecting more
|
||||
// (i.e., _entries.length < max),
|
||||
// but getEntries didn't return anything
|
||||
// (i.e., an entry doesn't exist).
|
||||
// So, let's create it.
|
||||
//
|
||||
// BAD CODE: THIS ASSUMES IT NEEDS TO BE TODAY
|
||||
// This will eventually break.
|
||||
//
|
||||
|
||||
Document new_today_entry = await getOne(date: formatter.format(DateTime.now()));
|
||||
|
||||
newEntries.add(new_today_entry);
|
||||
}
|
||||
|
||||
_entries.addAll(newEntries);
|
||||
notifyListeners();
|
||||
}
|
||||
_ready = true;
|
||||
print("Got ${_entries.length} entries");
|
||||
}
|
||||
|
||||
getOne({required String date}) async {
|
||||
try {
|
||||
// print(date);
|
||||
// print(_entries.last.$id);
|
||||
return _entries.singleWhere((element) => element.$id == date);
|
||||
} catch (e) {
|
||||
int offset =
|
||||
DateTime.parse(date).difference(DateTime.parse("2023-01-01")).inDays +
|
||||
1;
|
||||
|
||||
List<Document> response = await getEntries(
|
||||
limit: 1,
|
||||
paginationQuery: Query.offset(offset),
|
||||
);
|
||||
if(response.isEmpty){
|
||||
print("No entry in database; creating $date");
|
||||
Document newEntry = await addEntry(date: date);
|
||||
_entries.add(newEntry);
|
||||
notifyListeners();
|
||||
return(newEntry);
|
||||
}
|
||||
else{
|
||||
_entries.add(response.first);
|
||||
notifyListeners();
|
||||
return(response.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getEntries({int limit = 100, paginationQuery}) async {
|
||||
var response = await databases.listDocuments(
|
||||
databaseId: APPWRITE_DATABASE_ID,
|
||||
collectionId: COLLECTION,
|
||||
queries: [
|
||||
Query.orderAsc("date"),
|
||||
Query.offset(offset),
|
||||
paginationQuery,
|
||||
Query.limit(limit),
|
||||
]);
|
||||
return response.documents;
|
||||
}
|
||||
|
||||
Future<Document> updateEntry(
|
||||
{String? dateISO,
|
||||
List<dynamic>? hours,
|
||||
int? mood,
|
||||
String? comments}) async {
|
||||
String date = formatter.format(DateTime.parse(dateISO!));
|
||||
|
||||
int entryIndex = _entries.indexWhere((element) => elDate(element) == date);
|
||||
|
||||
hours ??= _entries[entryIndex].data['hours'];
|
||||
comments ??= _entries[entryIndex].data['comments'];
|
||||
mood ??= _entries[entryIndex].data['mood'];
|
||||
|
||||
Document newEntry = await databases.updateDocument(
|
||||
databaseId: APPWRITE_DATABASE_ID,
|
||||
collectionId: COLLECTION,
|
||||
documentId: date,
|
||||
data: {'hours': hours, 'mood': mood, 'comments': comments},
|
||||
);
|
||||
|
||||
print("Updated $date.");
|
||||
|
||||
_entries.removeAt(entryIndex);
|
||||
_entries.add(newEntry);
|
||||
notifyListeners();
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
updateHours(dayEntry, index, value) {
|
||||
List<dynamic> hours = dayEntry.data['hours'];
|
||||
try {
|
||||
hours[index] = num.parse(value);
|
||||
} catch (e) {
|
||||
if (hours.length == index) {
|
||||
hours.add(num.parse(value));
|
||||
} else {
|
||||
hours.addAll(List.generate(index - hours.length, (i) => -1));
|
||||
hours.add(num.parse(value));
|
||||
}
|
||||
}
|
||||
updateEntry(dateISO: dayEntry.data['date'], hours: hours);
|
||||
}
|
||||
|
||||
Future<Document> addEntry(
|
||||
{required String date,
|
||||
List hours = const [],
|
||||
int mood = -1,
|
||||
String comments = ""}) {
|
||||
dynamic mood,
|
||||
String comments = ""}) async {
|
||||
|
||||
return databases.createDocument(
|
||||
databaseId: APPWRITE_DATABASE_ID,
|
||||
collectionId: COLLECTION,
|
||||
documentId: date,
|
||||
data: {
|
||||
'date': DateTime.parse(date),
|
||||
'date': date,
|
||||
'hours': hours,
|
||||
'mood': mood,
|
||||
'comments': comments
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CategoryColor {
|
||||
black("273036"),
|
||||
blue("00a9b3"),
|
||||
red("c71634"),
|
||||
darkred("ff2816"),
|
||||
lime("bfff55"),
|
||||
green("189749"),
|
||||
pink("ff65ae"),
|
||||
purple("5b3ab1"),
|
||||
cyan("005744"),
|
||||
orange("ff6d01"),
|
||||
yellow("fff336");
|
||||
|
||||
const CategoryColor(this.hex);
|
||||
|
||||
final String hex;
|
||||
|
||||
name() {
|
||||
return toString().split('.').last;
|
||||
}
|
||||
|
||||
backgroundColor() {
|
||||
return Color(int.parse("0xff$hex"));
|
||||
}
|
||||
|
||||
foregroundColor() {
|
||||
return Color(int.parse("0xff$hex")).computeLuminance() > 0.3
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
const String APPWRITE_PROJECT_ID = "lifetracker";
|
||||
const String APPWRITE_DATABASE_ID = "lifetracker-db";
|
||||
const String CATEGORIES_DB = "categories";
|
||||
const String APPWRITE_URL = "https://db.ryanpandya.com/v1";
|
||||
const String COLLECTION = "ryan";
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
|
||||
String formatDate({String format = "", String? dateISO}) {
|
||||
final DateFormat dateFormatter = DateFormat(format);
|
||||
final date = dateISO!.isEmpty ? DateTime.now() : DateTime.parse(dateISO);
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
|
||||
String hourString(int e) {
|
||||
try {
|
||||
tz.getLocation('America/Los_Angeles');
|
||||
} catch (e) {
|
||||
tz.initializeTimeZones();
|
||||
}
|
||||
|
||||
int pacificTime = tz.TZDateTime.now(tz.getLocation('America/Los_Angeles'))
|
||||
.timeZoneOffset
|
||||
.inHours;
|
||||
int localTime = DateTime.now().timeZoneOffset.inHours;
|
||||
|
||||
e = e + localTime - pacificTime;
|
||||
|
||||
var meridien = "AM";
|
||||
var hour = 12;
|
||||
|
||||
if (e > 24) {
|
||||
hour = e - 24;
|
||||
} else if (e > 12) {
|
||||
hour = e - 12;
|
||||
} else if (e > 0) {
|
||||
hour = e;
|
||||
}
|
||||
if (e > 11 && e < 24) {
|
||||
meridien = "PM";
|
||||
}
|
||||
return "${hour.toString()} $meridien";
|
||||
}
|
||||
|
||||
Color moodColor(mood) {
|
||||
if (mood == null) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
if (mood >= 8) {
|
||||
return Colors.green;
|
||||
}
|
||||
if (mood > 5) {
|
||||
return Colors.blue;
|
||||
}
|
||||
if (mood == 5) {
|
||||
return Colors.yellow;
|
||||
}
|
||||
if (mood >= 3) {
|
||||
return Colors.amber;
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
+19
-93
@@ -1,12 +1,19 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ltx_flutter/pages/tabs_page.dart';
|
||||
import 'package:ltx_flutter/appwrite/appwrite.dart';
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:ltx_flutter/pages/login_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
runApp(ChangeNotifierProvider(
|
||||
create: (context) => AuthAPI(),
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => AuthAPI()),
|
||||
ChangeNotifierProvider(create: (context) => DatabaseAPI()),
|
||||
ChangeNotifierProvider(create: (context) => CategoriesAPI()),
|
||||
],
|
||||
child: LifetrackerApp(),
|
||||
));
|
||||
}
|
||||
@@ -16,19 +23,23 @@ class LifetrackerApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = context.watch<AuthAPI>().status;
|
||||
print('TOP CHANGE Value changed to: $value!');
|
||||
|
||||
final loginStatus = context.watch<AuthAPI>().status;
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
scrollBehavior: const MaterialScrollBehavior().copyWith(dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad,
|
||||
}),
|
||||
title: 'Lifetracker',
|
||||
theme: ThemeData.from(
|
||||
colorScheme: ColorScheme.dark(),
|
||||
),
|
||||
home: value == AuthStatus.uninitialized
|
||||
home: loginStatus == AuthStatus.uninitialized
|
||||
? const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: value == AuthStatus.authenticated
|
||||
: loginStatus == AuthStatus.authenticated
|
||||
? const TabsPage()
|
||||
: LoginPage());
|
||||
// DefaultTabController(
|
||||
@@ -55,88 +66,3 @@ class LifetrackerApp extends StatelessWidget {
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
AccountPageState createState() {
|
||||
return AccountPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class AccountPageState extends State<AccountPage> {
|
||||
final _loginFormKey = GlobalKey<FormState>();
|
||||
String email = "";
|
||||
String password = "";
|
||||
|
||||
void _logIn(context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logging in...')),
|
||||
);
|
||||
// client.login(email, password);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(30.0),
|
||||
child: Form(
|
||||
key: _loginFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: TextFormField(
|
||||
obscureText: false,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Email',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (value) => setState(() {
|
||||
email = value;
|
||||
}),
|
||||
)),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: TextFormField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Password',
|
||||
),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
onChanged: (value) => setState(() {
|
||||
password = value;
|
||||
}),
|
||||
)),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_logIn(context);
|
||||
},
|
||||
icon: Icon(Icons.login),
|
||||
label: Text("Log in")),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
email.isEmpty ? Text("No data") : Text(email),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
password.isEmpty ? Text("No Data") : Text(password),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class TodayPage extends StatefulWidget {
|
||||
const TodayPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TodayPageState createState() => _TodayPageState();
|
||||
}
|
||||
|
||||
class _TodayPageState extends State<TodayPage> {
|
||||
final database = DatabaseAPI();
|
||||
AuthStatus authStatus = AuthStatus.uninitialized;
|
||||
DateTime? date;
|
||||
Document? entry;
|
||||
|
||||
var hours = List<int>.generate(24, (i) => i).map((e) {
|
||||
var meridien = "AM";
|
||||
var hour = 12;
|
||||
if (e > 12) {
|
||||
hour = e - 12;
|
||||
} else if (e > 0) {
|
||||
hour = e;
|
||||
}
|
||||
if (e > 11) {
|
||||
meridien = "PM";
|
||||
}
|
||||
return {
|
||||
"string": "${hour.toString()} $meridien",
|
||||
"index": e.toInt(),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final AuthAPI appwrite = context.read<AuthAPI>();
|
||||
authStatus = appwrite.status;
|
||||
loadEntry();
|
||||
}
|
||||
|
||||
loadEntry() async {
|
||||
try {
|
||||
final value = await database.getEntries(limit: 1, paginationQuery: "");
|
||||
setState(() {
|
||||
entry = value.documents[0];
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
String formatDate({String format = "", String? dateISO}) {
|
||||
final DateFormat dateFormatter = DateFormat(format);
|
||||
final date = dateISO!.isEmpty ? DateTime.now() : DateTime.parse(dateISO);
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: entry == null
|
||||
? CircularProgressIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: TextStyle(
|
||||
fontSize: 40,
|
||||
),
|
||||
formatDate(
|
||||
format: "E, MMM dd, yyyy",
|
||||
dateISO: entry?.data['date'],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: hours.map(
|
||||
(e) {
|
||||
int h = e["index"] as int;
|
||||
List<dynamic> hoursData = entry!.data['hours'];
|
||||
String hourData = (h >= hoursData.length)
|
||||
? ""
|
||||
: hoursData[h].toString();
|
||||
return ListTile(
|
||||
leading: Text(e["string"] as String),
|
||||
title: Text(hourData),
|
||||
);
|
||||
},
|
||||
).toList()),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
children: List<int>.generate(10, (i) => i).map(
|
||||
(e) {
|
||||
var generatedColor =
|
||||
math.Random().nextInt(Colors.primaries.length);
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: FilledButton(
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all<
|
||||
RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
side: BorderSide(color: Colors.white))),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Colors.primaries[generatedColor]),
|
||||
),
|
||||
onLongPress: () =>
|
||||
print("Long pressed ${e.toString()}"),
|
||||
onPressed: () => print("Tapped ${e.toString()}"),
|
||||
child: Text(e.toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({Key? key}) : super(key: key);
|
||||
@@ -10,8 +14,50 @@ class AccountPage extends StatefulWidget {
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(child: Text("Account")),
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
Consumer<AuthAPI>(builder: (context, account, child) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.person),
|
||||
title: Text("Account"),
|
||||
trailing: Text("${account.username}"),
|
||||
);
|
||||
}),
|
||||
Consumer<DatabaseAPI>(builder: (context, entries, child) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.edit_note),
|
||||
title: Text("Entries"),
|
||||
trailing: Text("${entries.total()}"),
|
||||
);
|
||||
}),
|
||||
Consumer<CategoriesAPI>(builder: (context, categories, child) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.category),
|
||||
title: Text("Categories"),
|
||||
trailing: Text("${categories.total}"),
|
||||
);
|
||||
}),
|
||||
Center(
|
||||
child: Consumer<AuthAPI>(
|
||||
builder: (context, account, child) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: () => account.signOut(),
|
||||
icon: Icon(Icons.logout),
|
||||
label: Text("Log out"),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
|
||||
class CategoriesPage extends StatefulWidget {
|
||||
const CategoriesPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CategoriesPageState createState() => _CategoriesPageState();
|
||||
}
|
||||
|
||||
class _CategoriesPageState extends State<CategoriesPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NewCategoryPage(category: CategoryData()),
|
||||
),
|
||||
),
|
||||
tooltip: "New Category",
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
body: Consumer<CategoriesAPI>(builder: (context, categories, child) {
|
||||
return ListView.builder(
|
||||
itemCount: categories.total,
|
||||
itemBuilder: (context, i) {
|
||||
if (i < categories.total) {
|
||||
Category category = categories.get(i);
|
||||
if (!category.hasParent() && category.number != 0) {
|
||||
return Column(
|
||||
children: [
|
||||
Divider(
|
||||
height: 0,
|
||||
thickness: 2,
|
||||
),
|
||||
category.number < 0
|
||||
? SizedBox.shrink()
|
||||
: CategoryRow(category: category),
|
||||
],
|
||||
);
|
||||
}
|
||||
return CategoryRow(category: category);
|
||||
}
|
||||
else{
|
||||
print("Huh?");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryRow extends StatelessWidget {
|
||||
const CategoryRow({
|
||||
super.key,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
final Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPress: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NewCategoryPage(
|
||||
category: category.data(),
|
||||
)),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxHeight: 75, maxWidth: 50),
|
||||
color: category.backgroundColor,
|
||||
child: Center(
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: category.foregroundColor,
|
||||
fontSize: 18,
|
||||
),
|
||||
category.number.toString())),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: category.leftPadding()),
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
),
|
||||
category.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white38,
|
||||
),
|
||||
"${category.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NewCategoryPage extends StatefulWidget {
|
||||
const NewCategoryPage({Key? key, required this.category}) : super(key: key);
|
||||
|
||||
final CategoryData category;
|
||||
|
||||
@override
|
||||
State<NewCategoryPage> createState() => _NewCategoryPageState();
|
||||
}
|
||||
|
||||
class _NewCategoryPageState extends State<NewCategoryPage> {
|
||||
String _name = "";
|
||||
bool _isNew = true;
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isNew = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.category.number != null) {
|
||||
_isNew = false;
|
||||
}
|
||||
_name = _isNew ? "New Category" : "Edit Category";
|
||||
|
||||
final CategoriesAPI api = context.watch<CategoriesAPI>();
|
||||
saveCategory(form, context) {
|
||||
var name = form?.fields['categoryName'].value;
|
||||
var number = form?.fields['categoryNumber'].value;
|
||||
var color = form?.fields['categoryColor'].value;
|
||||
var description = form?.fields['categoryDescription'].value;
|
||||
var parent = form?.fields['categoryParent'].value;
|
||||
|
||||
if (_isNew) {
|
||||
api.addCategory(
|
||||
name: name,
|
||||
number: double.parse(number),
|
||||
color: color,
|
||||
description: description,
|
||||
parentId: int.parse(parent),
|
||||
);
|
||||
} else {
|
||||
api.updateCategory(
|
||||
name: name,
|
||||
number: double.parse(number),
|
||||
color: color,
|
||||
description: description,
|
||||
parentId: int.parse(parent),
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_name),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(left: 50, right: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilder(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 25),
|
||||
FormBuilderTextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Number",
|
||||
),
|
||||
name: 'categoryNumber',
|
||||
initialValue: widget.category.number.toString(),
|
||||
),
|
||||
SizedBox(height: 25),
|
||||
FormBuilderTextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name",
|
||||
),
|
||||
name: 'categoryName',
|
||||
initialValue: widget.category.name,
|
||||
),
|
||||
SizedBox(height: 25),
|
||||
Consumer<CategoriesAPI>(
|
||||
builder: (context, categories, child) {
|
||||
return FormBuilderDropdown(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Color",
|
||||
),
|
||||
initialValue: widget.category.color,
|
||||
items: [
|
||||
for (var c in categories.colors)
|
||||
DropdownMenuItem(
|
||||
value: c.hex,
|
||||
child: Container(
|
||||
decoration:
|
||||
BoxDecoration(color: c.backgroundColor()),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
c.name(),
|
||||
style: TextStyle(color: c.foregroundColor()),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
name: 'categoryColor',
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 25),
|
||||
FormBuilderTextField(
|
||||
name: 'categoryDescription',
|
||||
initialValue: widget.category.description,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Description",
|
||||
),
|
||||
),
|
||||
SizedBox(height: 25),
|
||||
FormBuilderTextField(
|
||||
keyboardType: TextInputType.number,
|
||||
name: 'categoryParent',
|
||||
initialValue: widget.category.parent.toString(),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Parent",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
OutlinedButton(
|
||||
onPressed: () => saveCategory(_formKey.currentState, context),
|
||||
child: Text("Save"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:appwrite/appwrite.dart';
|
||||
import 'package:ltx_flutter/appwrite/appwrite.dart';
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
@@ -98,6 +97,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
onSubmitted: (str) => signIn(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spreadsheet_table/spreadsheet_table.dart';
|
||||
import 'package:pluto_grid/pluto_grid.dart';
|
||||
import 'package:ltx_flutter/appwrite/appwrite.dart';
|
||||
import 'package:ltx_flutter/appwrite/auth_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
@@ -61,7 +61,7 @@ class _TablePageState extends State<TablePage> {
|
||||
title: hourTitle,
|
||||
field: "hours[$e]",
|
||||
type: PlutoColumnType.number(),
|
||||
width: 65,
|
||||
width: 70,
|
||||
enableContextMenu: false,
|
||||
enableDropToResize: false,
|
||||
textAlign: PlutoColumnTextAlign.center,
|
||||
@@ -75,23 +75,22 @@ class _TablePageState extends State<TablePage> {
|
||||
field: 'date',
|
||||
type: PlutoColumnType.text(),
|
||||
readOnly: true,
|
||||
width: 110,
|
||||
frozen: PlutoColumnFrozen.start,
|
||||
width: 70,
|
||||
enableContextMenu: false,
|
||||
enableDropToResize: false,
|
||||
textAlign: PlutoColumnTextAlign.center,
|
||||
titleTextAlign: PlutoColumnTextAlign.center,
|
||||
frozen: PlutoColumnFrozen.start,
|
||||
),
|
||||
PlutoColumn(
|
||||
title: 'DAY',
|
||||
field: 'day',
|
||||
type: PlutoColumnType.text(),
|
||||
readOnly: true,
|
||||
width: 53,
|
||||
enableContextMenu: false,
|
||||
enableDropToResize: false,
|
||||
),
|
||||
// PlutoColumn(
|
||||
// title: 'DAY',
|
||||
// field: 'day',
|
||||
// type: PlutoColumnType.text(),
|
||||
// readOnly: true,
|
||||
// width: 50,
|
||||
// frozen: PlutoColumnFrozen.start,
|
||||
// enableContextMenu: false,
|
||||
// enableDropToResize: false,
|
||||
// ),
|
||||
] +
|
||||
hourCols +
|
||||
[
|
||||
@@ -117,23 +116,27 @@ class _TablePageState extends State<TablePage> {
|
||||
var cells = {
|
||||
'date': PlutoCell(
|
||||
value: formatDate(
|
||||
format: "MM/DD E",
|
||||
format: "MM/DD",
|
||||
dateISO: e.data['date'],
|
||||
),
|
||||
),
|
||||
'day': PlutoCell(
|
||||
value: formatDate(
|
||||
format: "E",
|
||||
dateISO: e.data['date'],
|
||||
),
|
||||
),
|
||||
// 'day': PlutoCell(
|
||||
// value: formatDate(
|
||||
// format: "E",
|
||||
// dateISO: e.data['date'],
|
||||
// ),
|
||||
// ),
|
||||
'mood': PlutoCell(value: e.data['mood']),
|
||||
'comments': PlutoCell(value: e.data['comments']),
|
||||
};
|
||||
var hours = <String, PlutoCell>{};
|
||||
for (int i = 0; i < 24; i++) {
|
||||
hours.putIfAbsent(
|
||||
"hours[$i]", () => PlutoCell(value: e.data['hours'][i]));
|
||||
"hours[$i]",
|
||||
() => PlutoCell(
|
||||
value: e.data['hours'][i],
|
||||
),
|
||||
);
|
||||
}
|
||||
cells.addEntries(hours.entries);
|
||||
|
||||
@@ -159,8 +162,7 @@ class _TablePageState extends State<TablePage> {
|
||||
child: entries!.isEmpty
|
||||
? Center(child: const CircularProgressIndicator())
|
||||
: Scaffold(
|
||||
body: Container(
|
||||
padding: const EdgeInsets.all(0),
|
||||
body: InteractiveViewer(
|
||||
child: PlutoGrid(
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
@@ -252,7 +254,7 @@ class SpreadsheetWidget extends StatelessWidget {
|
||||
List hours = entry?.data['hours'];
|
||||
|
||||
if (col == 0) {
|
||||
return Center(child: Text('$day'));
|
||||
return Center(child: Text(day));
|
||||
} else if (col > 0 && col < 25) {
|
||||
return Center(child: Text(hours[col - 1].toString()));
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:ltx_flutter/pages/table_page.dart';
|
||||
import 'package:ltx_flutter/pages/account_page.dart';
|
||||
import 'package:ltx_flutter/pages/categories_page.dart';
|
||||
import 'package:ltx_flutter/pages/today_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ltx_flutter/pages/today_views/infinity_view.dart';
|
||||
import '../app_properties_bloc.dart';
|
||||
|
||||
class TabsPage extends StatefulWidget {
|
||||
const TabsPage({Key? key}) : super(key: key);
|
||||
@@ -12,7 +15,7 @@ class TabsPage extends StatefulWidget {
|
||||
class _TabsPageState extends State<TabsPage> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
static const _widgets = [TablePage(), AccountPage()];
|
||||
static const _widgets = [TodayPage(), InfinityView(), CategoriesPage(), AccountPage()];
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
@@ -23,14 +26,24 @@ class _TabsPageState extends State<TabsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Lifetracker"),
|
||||
),
|
||||
// appBar: AppBar(
|
||||
// title: StreamBuilder<Object>(
|
||||
// stream: appBloc.titleStream,
|
||||
// initialData: "Flutter",
|
||||
// builder: (context, snapshot) {
|
||||
// return Text(snapshot.data.toString());
|
||||
// }),
|
||||
// ),
|
||||
body: _widgets.elementAt(_selectedIndex),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.table_chart_outlined), label: "Tracker"),
|
||||
icon: Icon(Icons.today_outlined), label: "Today"),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.all_inclusive), label: "Grid"),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.category_outlined), label: "Categories"),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.account_circle_outlined), label: "Account")
|
||||
],
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'today_views/day_view.dart';
|
||||
import 'today_views/infinity_view.dart';
|
||||
import 'today_views/week_view.dart';
|
||||
|
||||
class TodayPage extends StatefulWidget {
|
||||
const TodayPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TodayPageState createState() => _TodayPageState();
|
||||
}
|
||||
|
||||
class _TodayPageState extends State<TodayPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
if (constraints.maxWidth > 800) {
|
||||
return Text("Wide");
|
||||
} else {
|
||||
return Consumer<DatabaseAPI>(builder: (context, database, child) {
|
||||
return database.ready
|
||||
? NarrowView()
|
||||
: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: database.progress,
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class NarrowView extends StatelessWidget {
|
||||
const NarrowView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
initialIndex: 0,
|
||||
child: DayView()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:ltx_flutter/helpers.dart';
|
||||
|
||||
class DayView extends StatefulWidget {
|
||||
const DayView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DayView> createState() => _DayViewState();
|
||||
}
|
||||
|
||||
class _DayViewState extends State<DayView> {
|
||||
DateTime _date = DateTime.now();
|
||||
|
||||
late DatabaseAPI database;
|
||||
late CategoriesAPI categories;
|
||||
|
||||
late List<Document> entries = [];
|
||||
late Document? dayEntry;
|
||||
late List<num> hours = [];
|
||||
late num? mood = 0;
|
||||
late String comments = "";
|
||||
|
||||
bool _editable = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
database = context.watch<DatabaseAPI>();
|
||||
categories = context.watch<CategoriesAPI>();
|
||||
entries = database.entries;
|
||||
}
|
||||
|
||||
void _incrementDate(int amount) =>
|
||||
_setDate(_date.add(Duration(days: amount)));
|
||||
|
||||
void _setDate(DateTime? day) {
|
||||
if (day != null) {
|
||||
setState(() {
|
||||
_date = day;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void getDayEntry() async{
|
||||
List<Document> entries = database.entries;
|
||||
String formattedDate = formatDate(
|
||||
dateISO: _date.toIso8601String(),
|
||||
format: "yyyy-MM-dd",
|
||||
);
|
||||
try {
|
||||
dayEntry = entries.singleWhere((element) => element.$id == formattedDate);
|
||||
String date = formatDate(
|
||||
format: "LLL d", dateISO: dayEntry?.data['date'].toString());
|
||||
print("Got entry for $date");
|
||||
} catch (e) {
|
||||
dayEntry = entries.last;
|
||||
print("Got entry for ${entries.last.$id}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
getDayEntry();
|
||||
|
||||
setState(() {
|
||||
if (dayEntry != null) {
|
||||
String? newComment = dayEntry?.data["comments"];
|
||||
if(newComment == null){
|
||||
newComment = "";
|
||||
}
|
||||
comments = newComment;
|
||||
mood = dayEntry?.data["mood"];
|
||||
}
|
||||
});
|
||||
|
||||
TextEditingController commentsController =
|
||||
TextEditingController(text: comments);
|
||||
|
||||
Widget moodWidget = mood == null
|
||||
? Icon(
|
||||
size: 30,
|
||||
Icons.star_outline,
|
||||
color: Colors.white,
|
||||
)
|
||||
: Text(
|
||||
style: TextStyle(fontSize: 20, color: Colors.black),
|
||||
mood.toString());
|
||||
|
||||
return GestureDetector(
|
||||
onHorizontalDragEnd: (details) {
|
||||
if (details.primaryVelocity!.abs() > 10) {
|
||||
details.primaryVelocity! < 0 ? _incrementDate(1) : _incrementDate(-1);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _setDate(DateTime.now()),
|
||||
onTap: () {
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: _date,
|
||||
firstDate: DateTime.parse("2023-01-01"),
|
||||
lastDate: DateTime.now().add(Duration(days: 7)),
|
||||
).then((value) => _setDate(value));
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: "Previous day",
|
||||
icon: Icon(Icons.arrow_left),
|
||||
onPressed: () => _incrementDate(-1),
|
||||
),
|
||||
SizedBox(
|
||||
width: 0.5 * MediaQuery.of(context).size.width,
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
formatDate(
|
||||
format: 'EEEEE, LLLL dd, yyyy',
|
||||
dateISO: _date.toIso8601String()),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: "Next day",
|
||||
icon: Icon(Icons.arrow_right),
|
||||
onPressed: () => _incrementDate(1),
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(width: 5),
|
||||
),
|
||||
Text("Edit"),
|
||||
Switch(
|
||||
value: _editable,
|
||||
onChanged: (value) => setState(() {
|
||||
_editable = !_editable;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: categories.ready
|
||||
? HourGenerator(
|
||||
dayEntry: dayEntry,
|
||||
editable: _editable,
|
||||
)
|
||||
: ListView(
|
||||
children: [Center(child: CircularProgressIndicator())]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30, right: 30, left: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 10),
|
||||
SizedBox.square(
|
||||
dimension: 50,
|
||||
child: GestureDetector(
|
||||
onTapDown: (event) => showMenu(
|
||||
initialValue: mood,
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
event.globalPosition.dx + 25,
|
||||
event.globalPosition.dy - 50,
|
||||
MediaQuery.of(context).size.width -
|
||||
event.globalPosition.dx,
|
||||
MediaQuery.of(context).size.height -
|
||||
event.globalPosition.dy +
|
||||
0,
|
||||
),
|
||||
items: List.generate(
|
||||
10,
|
||||
(index) => PopupMenuItem(
|
||||
onTap: () => database.updateEntry(
|
||||
dateISO: dayEntry!.data['date'], mood: index),
|
||||
value: index,
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: moodColor(index),
|
||||
),
|
||||
index.toString(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: mood == null
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white)),
|
||||
child: Center(child: moodWidget),
|
||||
)
|
||||
: Container(
|
||||
color: moodColor(mood),
|
||||
child: Center(child: moodWidget),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 30),
|
||||
Expanded(
|
||||
child: _editable
|
||||
? TextFormField(
|
||||
decoration: InputDecoration(hintText: "Comments"),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
smartQuotesType: SmartQuotesType.enabled,
|
||||
enableInteractiveSelection: true,
|
||||
controller: commentsController,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
onFieldSubmitted: (value) {
|
||||
database.updateEntry(
|
||||
dateISO: dayEntry!.data['date'],
|
||||
comments: value);
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
style: TextStyle(fontSize: 16),
|
||||
comments.isEmpty ? "No comments yet." : comments),
|
||||
),
|
||||
SizedBox(width: 30),
|
||||
_editable
|
||||
? IconButton(
|
||||
tooltip: "Save",
|
||||
onPressed: () => database.updateEntry(
|
||||
dateISO: dayEntry!.data['date'],
|
||||
comments: commentsController.value.text.toString()),
|
||||
icon: Icon(Icons.save),
|
||||
)
|
||||
: Container()
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HourGenerator extends StatefulWidget {
|
||||
const HourGenerator(
|
||||
{super.key, required this.dayEntry, required this.editable});
|
||||
|
||||
final Document? dayEntry;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<HourGenerator> createState() => _HourGeneratorState();
|
||||
}
|
||||
|
||||
class _HourGeneratorState extends State<HourGenerator> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> generateHours(entry, bool edit) {
|
||||
if (entry == null) {
|
||||
return [Center(child: RefreshProgressIndicator())];
|
||||
}
|
||||
List<dynamic> hours = entry.data['hours'];
|
||||
|
||||
// print(hours);
|
||||
|
||||
List reduced = [];
|
||||
int counter = 0;
|
||||
|
||||
if (!edit) {
|
||||
for (num val in hours) {
|
||||
if (reduced.isEmpty) {
|
||||
reduced.add({'val': val, 'num': 1, 'hour': counter});
|
||||
} else {
|
||||
if (reduced.last['val'] == val) {
|
||||
reduced.last['num']++;
|
||||
reduced.last['hour'] = counter;
|
||||
} else {
|
||||
reduced.add({'val': val, 'num': 1, 'hour': counter});
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
} else {
|
||||
for (num val in hours) {
|
||||
reduced.add({
|
||||
'hour': counter,
|
||||
'val': val,
|
||||
'num': 1,
|
||||
});
|
||||
if (edit) counter++;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> hourWidgets = reduced.map(
|
||||
(e) {
|
||||
double height = double.parse((e['num'] * 36).toString());
|
||||
return Consumer<CategoriesAPI>(
|
||||
builder: (context, categories, child) {
|
||||
Category category = categories.lookUp(e['val'].toString());
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
edit = true;
|
||||
});
|
||||
},
|
||||
child: Card(
|
||||
color: category.backgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
e['num'] == 1
|
||||
? SizedBox(
|
||||
width: 50,
|
||||
child: Center(
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
color: category.foregroundColor),
|
||||
hourString(e['hour']),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5, top: 10, bottom: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: TextStyle(
|
||||
color: category.foregroundColor),
|
||||
hourString(e['hour'] - e['num'] + 1)),
|
||||
Expanded(
|
||||
child: VerticalDivider(
|
||||
indent: 2,
|
||||
endIndent: 2,
|
||||
color: category.foregroundColor,
|
||||
thickness: 2,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
style: TextStyle(
|
||||
color: category.foregroundColor),
|
||||
hourString(e['hour'] + 1))
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: edit
|
||||
? HourFormField(
|
||||
category: category,
|
||||
index: e['hour'],
|
||||
dayEntry: entry,
|
||||
)
|
||||
: Text(
|
||||
style: TextStyle(
|
||||
color: category.foregroundColor),
|
||||
category.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
List<Consumer<CategoriesAPI>> remainingHours =
|
||||
List.generate(24 - counter, (index) {
|
||||
return Consumer<CategoriesAPI>(builder: (context, value, child) {
|
||||
return SizedBox(
|
||||
height: 35,
|
||||
child: Card(
|
||||
color: Colors.black,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Center(
|
||||
child: Text(
|
||||
style: TextStyle(color: Colors.white),
|
||||
hourString(counter + index)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: HourFormField(
|
||||
index: index + counter,
|
||||
dayEntry: entry,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
List<Consumer<CategoriesAPI>> remainingHoursConsolidated = [
|
||||
Consumer<CategoriesAPI>(
|
||||
builder: (context, value, child) {
|
||||
return SizedBox(
|
||||
height: (24 - counter) * 30,
|
||||
child: Card(
|
||||
color: Colors.black,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: TextStyle(color: Colors.white),
|
||||
hourString(counter)),
|
||||
Expanded(
|
||||
child: VerticalDivider(
|
||||
indent: 2,
|
||||
endIndent: 2,
|
||||
color: Colors.white,
|
||||
thickness: 2,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
style: TextStyle(color: Colors.white),
|
||||
hourString(24))
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Icon(Icons.question_mark),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
|
||||
edit || counter == 23
|
||||
? hourWidgets.addAll(remainingHours)
|
||||
: hourWidgets.addAll(remainingHoursConsolidated);
|
||||
return hourWidgets;
|
||||
}
|
||||
|
||||
return FocusTraversalGroup(
|
||||
child:
|
||||
ListView(children: generateHours(widget.dayEntry, widget.editable)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HourFormField extends StatefulWidget {
|
||||
HourFormField({
|
||||
super.key,
|
||||
this.category,
|
||||
required this.index,
|
||||
required this.dayEntry,
|
||||
});
|
||||
|
||||
Category? category;
|
||||
int index;
|
||||
Document dayEntry;
|
||||
|
||||
@override
|
||||
State<HourFormField> createState() => _HourFormFieldState();
|
||||
}
|
||||
|
||||
class _HourFormFieldState extends State<HourFormField> {
|
||||
Color fgColor = Colors.white;
|
||||
|
||||
String catNum = "";
|
||||
|
||||
String catName = "[Empty]";
|
||||
FocusNode _node = FocusNode();
|
||||
late TextEditingController _controller;
|
||||
@override
|
||||
void initState() {
|
||||
_node.addListener(() {
|
||||
if (_node.hasFocus) {
|
||||
_controller.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: _controller.text.length);
|
||||
}
|
||||
});
|
||||
if (widget.category != null) {
|
||||
catNum = widget.dayEntry!.data['hours'][widget.index].toString();
|
||||
fgColor = widget.category!.foregroundColor;
|
||||
catName = widget.category!.name;
|
||||
}
|
||||
|
||||
_controller = TextEditingController.fromValue(
|
||||
TextEditingValue(
|
||||
text: catNum,
|
||||
),
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.category != null) {
|
||||
catNum = widget.category!.number.toString();
|
||||
fgColor = widget.category!.foregroundColor;
|
||||
catName = widget.category!.name;
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Consumer<DatabaseAPI>(builder: (context, database, child) {
|
||||
// return TextField(
|
||||
// controller: _controller,
|
||||
// focusNode: _node,
|
||||
// onEditingComplete: () async {
|
||||
// String value = _controller.value.text;
|
||||
// database.updateHours(widget.dayEntry, widget.index, value);
|
||||
|
||||
// _node.nextFocus();
|
||||
// },
|
||||
// keyboardType: TextInputType.number,
|
||||
// textAlign: TextAlign.center,
|
||||
// );
|
||||
return Consumer<CategoriesAPI>(
|
||||
builder: (context, categories, child) {
|
||||
return TextFormField(
|
||||
showCursor: false,
|
||||
autofillHints: [],
|
||||
key: Key(
|
||||
"hourField-${widget.dayEntry.$id}-${widget.index.toString()}"),
|
||||
textInputAction: TextInputAction.next,
|
||||
onEditingComplete: () {
|
||||
String value;
|
||||
String current = _controller.value.text;
|
||||
if(current == ""){
|
||||
value = getPreviousHourValue(widget);
|
||||
}
|
||||
else{
|
||||
value = current;
|
||||
}
|
||||
database.updateHours(widget.dayEntry, widget.index, value);
|
||||
setState(() {
|
||||
widget.category = categories.lookUp(num.parse(value));
|
||||
catName = widget.category!.name;
|
||||
});
|
||||
_node.nextFocus();
|
||||
},
|
||||
style: TextStyle(
|
||||
color: fgColor, fontFamily: "Monospace", height: 35),
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
),
|
||||
controller: _controller,
|
||||
focusNode: _node,
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
SizedBox(width: 40),
|
||||
Expanded(
|
||||
child: Text(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: fgColor, fontFamily: "Monospace"),
|
||||
catName),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
String getPreviousHourValue(widget) {
|
||||
int prevIndex = widget.index - 1;
|
||||
if(prevIndex < 0){ return "0";}
|
||||
else{
|
||||
return widget.dayEntry.data['hours'][prevIndex].toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:ltx_flutter/helpers.dart';
|
||||
|
||||
class InfinityView extends StatefulWidget {
|
||||
const InfinityView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InfinityView> createState() => _InfinityViewState();
|
||||
}
|
||||
|
||||
class _InfinityViewState extends State<InfinityView> {
|
||||
DateTime _date = DateTime.now();
|
||||
|
||||
late DatabaseAPI database;
|
||||
late CategoriesAPI categories;
|
||||
|
||||
late List<Document> entries = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
database = context.watch<DatabaseAPI>();
|
||||
categories = context.watch<CategoriesAPI>();
|
||||
entries = database.entries;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: GridView.builder(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 24),
|
||||
padding: EdgeInsets.all(2),
|
||||
itemBuilder: (context, index) {
|
||||
int entryIndex = (-1 + (index / 24)).toInt();
|
||||
if (index < 24) {
|
||||
// Build the top row
|
||||
return Text("");
|
||||
} else if (entryIndex < entries.length) {
|
||||
Document entry = entries[entryIndex];
|
||||
if (index % 24 == 0) {
|
||||
// First column, show date
|
||||
return Text(
|
||||
formatDate(
|
||||
dateISO: (entry.data['date']).toString(),
|
||||
format: "MM/dd"),
|
||||
);
|
||||
} else {
|
||||
int hourIndex = (index % 24) - 1;
|
||||
num? hour;
|
||||
try {
|
||||
hour = entry.data['hours'][hourIndex];
|
||||
} catch (e) {
|
||||
hour = null;
|
||||
}
|
||||
|
||||
String tooltip;
|
||||
Color fgColor;
|
||||
Color bgColor;
|
||||
|
||||
try {
|
||||
Category category = categories.lookUp(hour.toString());
|
||||
tooltip = category.name;
|
||||
fgColor = category.foregroundColor;
|
||||
bgColor = category.backgroundColor;
|
||||
} catch (e) {
|
||||
tooltip = "";
|
||||
fgColor = Colors.white;
|
||||
bgColor = Colors.black;
|
||||
}
|
||||
return SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: Container(
|
||||
color: bgColor,
|
||||
child: Center(
|
||||
child: Text(
|
||||
style: TextStyle(fontSize: 10, color: bgColor),
|
||||
hour.toString())),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:ltx_flutter/helpers.dart';
|
||||
|
||||
class WeekView extends StatelessWidget {
|
||||
const WeekView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
showCursor: false,
|
||||
autofillHints: [],
|
||||
autocorrect: false,
|
||||
onEditingComplete: () {
|
||||
print("Test");
|
||||
},
|
||||
style:
|
||||
TextStyle(color: Colors.amber, fontFamily: "Monospace", height: 35),
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ description: A new Flutter project.
|
||||
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
version: 0.0.1+1
|
||||
version: 0.1.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.4 <4.0.0'
|
||||
@@ -17,6 +17,9 @@ dependencies:
|
||||
flutter_svg: ^2.0.6
|
||||
intl: ^0.18.1
|
||||
pluto_grid: ^7.0.2
|
||||
string_to_hex: ^0.2.2
|
||||
flutter_form_builder: ^9.0.0
|
||||
timezone: ^0.9.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user