Compare commits
23
Commits
0dd56cfaa6
..
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 |
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"
|
applicationId "com.ryanpandya.lifetracker"
|
||||||
// You can update the following values to match your application needs.
|
// 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.
|
// 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
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:label="ltx_flutter"
|
android:label="Lifetracker"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<!-- Add this inside the `<application>` tag, along side the existing `<activity>` tags -->
|
<!-- 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,115 @@
|
|||||||
import 'package:appwrite/appwrite.dart';
|
import 'package:appwrite/appwrite.dart';
|
||||||
import 'package:appwrite/models.dart';
|
import 'package:appwrite/models.dart';
|
||||||
import 'package:ltx_flutter/constants/constants.dart';
|
import 'package:ltx_flutter/constants/constants.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:ltx_flutter/constants/colors.dart';
|
||||||
import 'package:ltx_flutter/appwrite/appwrite.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 {
|
class CategoriesAPI extends ChangeNotifier {
|
||||||
Client client = Client();
|
Client client = Client();
|
||||||
late final Account account;
|
late final Account account;
|
||||||
late final Databases databases;
|
late final Databases databases;
|
||||||
late final String userId;
|
late List<Document> _categories = [];
|
||||||
final AuthAPI auth = AuthAPI();
|
bool _ready = false;
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
CategoriesAPI() {
|
CategoriesAPI() {
|
||||||
init();
|
init();
|
||||||
|
getCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadUser() async {
|
// Getters
|
||||||
// try {
|
get total => _categories.length;
|
||||||
// user = await account.get();
|
get all => _categories;
|
||||||
// notifyListeners();
|
get isEmpty => _categories.isEmpty;
|
||||||
// } catch (e) {
|
get(n) => Category(_categories[n]);
|
||||||
// print(e);
|
get colors => CategoryColor.values;
|
||||||
// notifyListeners();
|
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() {
|
init() {
|
||||||
client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID);
|
client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID);
|
||||||
@@ -32,24 +117,31 @@ class CategoriesAPI extends ChangeNotifier {
|
|||||||
databases = Databases(client);
|
databases = Databases(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DocumentList> getCategories() {
|
getCategories() async {
|
||||||
print("Getting categories");
|
print("Updating categories.");
|
||||||
return databases.listDocuments(
|
_categories = [];
|
||||||
|
var response = await databases.listDocuments(
|
||||||
databaseId: CATEGORIES_DB,
|
databaseId: CATEGORIES_DB,
|
||||||
collectionId: COLLECTION,
|
collectionId: COLLECTION,
|
||||||
queries: [
|
queries: [
|
||||||
|
Query.limit(100),
|
||||||
|
Query.orderAsc("parent"),
|
||||||
Query.orderAsc("number"),
|
Query.orderAsc("number"),
|
||||||
]);
|
]);
|
||||||
|
_categories = response.documents;
|
||||||
|
notifyListeners();
|
||||||
|
_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Document> addCategory({
|
Future<Document>? addCategory({
|
||||||
required String name,
|
required String name,
|
||||||
required double number,
|
required double number,
|
||||||
required String color,
|
required String color,
|
||||||
String? description,
|
String? description,
|
||||||
int? parentId,
|
int? parentId,
|
||||||
}) {
|
}) async {
|
||||||
return databases.createDocument(
|
try {
|
||||||
|
return await databases.createDocument(
|
||||||
databaseId: CATEGORIES_DB,
|
databaseId: CATEGORIES_DB,
|
||||||
collectionId: COLLECTION,
|
collectionId: COLLECTION,
|
||||||
documentId: "category-${number.toString()}",
|
documentId: "category-${number.toString()}",
|
||||||
@@ -60,6 +152,41 @@ class CategoriesAPI extends ChangeNotifier {
|
|||||||
'parent': parentId,
|
'parent': parentId,
|
||||||
'description': description,
|
'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}) {
|
Future<dynamic> deleteCategory({required double number}) {
|
||||||
|
|||||||
@@ -1,63 +1,231 @@
|
|||||||
import 'package:appwrite/appwrite.dart';
|
import 'package:appwrite/appwrite.dart';
|
||||||
import 'package:appwrite/models.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:ltx_flutter/constants/constants.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DatabaseAPI {
|
class DatabaseAPI extends ChangeNotifier {
|
||||||
Client client = Client();
|
Client client = Client();
|
||||||
late final Account account;
|
late final Account account;
|
||||||
late final Databases databases;
|
late final Databases databases;
|
||||||
|
late final Realtime realtime;
|
||||||
final AuthAPI auth = AuthAPI();
|
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');
|
final DateFormat formatter = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
// Constructor
|
||||||
DatabaseAPI() {
|
DatabaseAPI() {
|
||||||
init();
|
init();
|
||||||
|
getAll();
|
||||||
|
subscribeRealtime();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
client
|
client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID);
|
||||||
.setEndpoint(APPWRITE_URL)
|
|
||||||
.setProject(APPWRITE_PROJECT_ID)
|
|
||||||
.setSelfSigned();
|
|
||||||
account = Account(client);
|
account = Account(client);
|
||||||
databases = Databases(client);
|
databases = Databases(client);
|
||||||
|
realtime = Realtime(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DocumentList> getEntries({int limit = 100, String dateISO = ""}) {
|
elDate(dynamic el) {
|
||||||
if (dateISO == "") {
|
return formatter.format(DateTime.parse(el.data['date']));
|
||||||
dateISO = DateTime.now().toIso8601String();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
var referenceDate = DateTime.parse("2023-01-01");
|
||||||
|
|
||||||
final date = DateTime.parse(dateISO);
|
final date = DateTime.parse(dateISO);
|
||||||
final offset = date.difference(referenceDate).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,
|
databaseId: APPWRITE_DATABASE_ID,
|
||||||
collectionId: COLLECTION,
|
collectionId: COLLECTION,
|
||||||
queries: [
|
queries: [
|
||||||
Query.orderAsc("date"),
|
Query.orderAsc("date"),
|
||||||
Query.offset(offset),
|
paginationQuery,
|
||||||
Query.limit(limit),
|
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(
|
Future<Document> addEntry(
|
||||||
{required String date,
|
{required String date,
|
||||||
List hours = const [],
|
List hours = const [],
|
||||||
int mood = -1,
|
dynamic mood,
|
||||||
String comments = ""}) {
|
String comments = ""}) async {
|
||||||
|
|
||||||
return databases.createDocument(
|
return databases.createDocument(
|
||||||
databaseId: APPWRITE_DATABASE_ID,
|
databaseId: APPWRITE_DATABASE_ID,
|
||||||
collectionId: COLLECTION,
|
collectionId: COLLECTION,
|
||||||
documentId: date,
|
documentId: date,
|
||||||
data: {
|
data: {
|
||||||
'date': DateTime.parse(date),
|
'date': date,
|
||||||
'hours': hours,
|
'hours': hours,
|
||||||
'mood': mood,
|
'mood': mood,
|
||||||
'comments': comments
|
'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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:flutter/material.dart';
|
||||||
import 'package:ltx_flutter/pages/tabs_page.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:ltx_flutter/pages/login_page.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(ChangeNotifierProvider(
|
runApp(MultiProvider(
|
||||||
create: (context) => AuthAPI(),
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (context) => AuthAPI()),
|
||||||
|
ChangeNotifierProvider(create: (context) => DatabaseAPI()),
|
||||||
|
ChangeNotifierProvider(create: (context) => CategoriesAPI()),
|
||||||
|
],
|
||||||
child: LifetrackerApp(),
|
child: LifetrackerApp(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -16,19 +23,23 @@ class LifetrackerApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final value = context.watch<AuthAPI>().status;
|
final loginStatus = context.watch<AuthAPI>().status;
|
||||||
print('TOP CHANGE Value changed to: $value!');
|
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
scrollBehavior: const MaterialScrollBehavior().copyWith(dragDevices: {
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
PointerDeviceKind.trackpad,
|
||||||
|
}),
|
||||||
title: 'Lifetracker',
|
title: 'Lifetracker',
|
||||||
theme: ThemeData.from(
|
theme: ThemeData.from(
|
||||||
colorScheme: ColorScheme.dark(),
|
colorScheme: ColorScheme.dark(),
|
||||||
),
|
),
|
||||||
home: value == AuthStatus.uninitialized
|
home: loginStatus == AuthStatus.uninitialized
|
||||||
? const Scaffold(
|
? const Scaffold(
|
||||||
body: Center(child: CircularProgressIndicator()),
|
body: Center(child: CircularProgressIndicator()),
|
||||||
)
|
)
|
||||||
: value == AuthStatus.authenticated
|
: loginStatus == AuthStatus.authenticated
|
||||||
? const TabsPage()
|
? const TabsPage()
|
||||||
: LoginPage());
|
: LoginPage());
|
||||||
// DefaultTabController(
|
// 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: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 {
|
class AccountPage extends StatefulWidget {
|
||||||
const AccountPage({Key? key}) : super(key: key);
|
const AccountPage({Key? key}) : super(key: key);
|
||||||
@@ -10,8 +14,50 @@ class AccountPage extends StatefulWidget {
|
|||||||
class _AccountPageState extends State<AccountPage> {
|
class _AccountPageState extends State<AccountPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Column(
|
||||||
body: Center(child: Text("Account")),
|
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"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
import 'package:ltx_flutter/appwrite/categories_api.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appwrite/models.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
|
||||||
class CategoriesPage extends StatefulWidget {
|
class CategoriesPage extends StatefulWidget {
|
||||||
const CategoriesPage({Key? key}) : super(key: key);
|
const CategoriesPage({Key? key}) : super(key: key);
|
||||||
@@ -10,53 +11,257 @@ class CategoriesPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CategoriesPageState extends State<CategoriesPage> {
|
class _CategoriesPageState extends State<CategoriesPage> {
|
||||||
final api = CategoriesAPI();
|
|
||||||
late List<Document>? categories = [];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
loadCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future loadCategories() async {
|
|
||||||
try {
|
|
||||||
final value = await api.getCategories();
|
|
||||||
setState(() {
|
|
||||||
categories = value.documents;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RefreshIndicator(
|
return Scaffold(
|
||||||
onRefresh: loadCategories,
|
floatingActionButton: FloatingActionButton(
|
||||||
child: Center(
|
onPressed: () => Navigator.push(
|
||||||
child: ListView.builder(
|
context,
|
||||||
itemCount: categories!.length + 1,
|
MaterialPageRoute(
|
||||||
itemBuilder: (context, index) {
|
builder: (context) => NewCategoryPage(category: CategoryData()),
|
||||||
if (index >= categories!.length || categories!.isEmpty) {
|
),
|
||||||
return ListTile(
|
),
|
||||||
leading: Text("New Category"),
|
tooltip: "New Category",
|
||||||
);
|
child: Icon(Icons.add),
|
||||||
} else {
|
),
|
||||||
Document? category = categories?[index];
|
body: Consumer<CategoriesAPI>(builder: (context, categories, child) {
|
||||||
Color backgroundColor =
|
return ListView.builder(
|
||||||
Color(int.parse("0x${category!.data['color']}"));
|
itemCount: categories.total,
|
||||||
Color textColor = backgroundColor.computeLuminance() > 0.2
|
itemBuilder: (context, i) {
|
||||||
? Colors.black
|
if (i < categories.total) {
|
||||||
: Colors.white;
|
Category category = categories.get(i);
|
||||||
return ListTile(
|
if (!category.hasParent() && category.number != 0) {
|
||||||
tileColor: backgroundColor,
|
return Column(
|
||||||
textColor: textColor,
|
children: [
|
||||||
leading: Text(category.data['number'].toString()),
|
Divider(
|
||||||
title: Text(category.data['name']),
|
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,5 +1,5 @@
|
|||||||
import 'package:appwrite/appwrite.dart';
|
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/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
|
onSubmitted: (str) => signIn(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spreadsheet_table/spreadsheet_table.dart';
|
import 'package:spreadsheet_table/spreadsheet_table.dart';
|
||||||
import 'package:pluto_grid/pluto_grid.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:ltx_flutter/appwrite/database_api.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:appwrite/models.dart';
|
import 'package:appwrite/models.dart';
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:ltx_flutter/pages/account_page.dart';
|
|||||||
import 'package:ltx_flutter/pages/categories_page.dart';
|
import 'package:ltx_flutter/pages/categories_page.dart';
|
||||||
import 'package:ltx_flutter/pages/today_page.dart';
|
import 'package:ltx_flutter/pages/today_page.dart';
|
||||||
import 'package:flutter/material.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 {
|
class TabsPage extends StatefulWidget {
|
||||||
const TabsPage({Key? key}) : super(key: key);
|
const TabsPage({Key? key}) : super(key: key);
|
||||||
@@ -13,7 +15,7 @@ class TabsPage extends StatefulWidget {
|
|||||||
class _TabsPageState extends State<TabsPage> {
|
class _TabsPageState extends State<TabsPage> {
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
static const _widgets = [TodayPage(), CategoriesPage(), AccountPage()];
|
static const _widgets = [TodayPage(), InfinityView(), CategoriesPage(), AccountPage()];
|
||||||
|
|
||||||
void _onItemTapped(int index) {
|
void _onItemTapped(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -24,14 +26,22 @@ class _TabsPageState extends State<TabsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
// appBar: AppBar(
|
||||||
title: Text("LTX Android"),
|
// title: StreamBuilder<Object>(
|
||||||
),
|
// stream: appBloc.titleStream,
|
||||||
|
// initialData: "Flutter",
|
||||||
|
// builder: (context, snapshot) {
|
||||||
|
// return Text(snapshot.data.toString());
|
||||||
|
// }),
|
||||||
|
// ),
|
||||||
body: _widgets.elementAt(_selectedIndex),
|
body: _widgets.elementAt(_selectedIndex),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
items: const [
|
items: const [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.today_outlined), label: "Today"),
|
icon: Icon(Icons.today_outlined), label: "Today"),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.all_inclusive), label: "Grid"),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.category_outlined), label: "Categories"),
|
icon: Icon(Icons.category_outlined), label: "Categories"),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:ltx_flutter/appwrite/appwrite.dart';
|
|
||||||
import 'package:ltx_flutter/appwrite/database_api.dart';
|
import 'package:ltx_flutter/appwrite/database_api.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:appwrite/models.dart';
|
import 'today_views/day_view.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'today_views/infinity_view.dart';
|
||||||
|
import 'today_views/week_view.dart';
|
||||||
|
|
||||||
class TodayPage extends StatefulWidget {
|
class TodayPage extends StatefulWidget {
|
||||||
const TodayPage({Key? key}) : super(key: key);
|
const TodayPage({Key? key}) : super(key: key);
|
||||||
@@ -13,87 +13,37 @@ class TodayPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TodayPageState extends State<TodayPage> {
|
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
|
@override
|
||||||
void initState() {
|
Widget build(BuildContext context) {
|
||||||
super.initState();
|
return LayoutBuilder(
|
||||||
final AuthAPI appwrite = context.read<AuthAPI>();
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
authStatus = appwrite.status;
|
if (constraints.maxWidth > 800) {
|
||||||
loadEntry();
|
return Text("Wide");
|
||||||
}
|
} else {
|
||||||
|
return Consumer<DatabaseAPI>(builder: (context, database, child) {
|
||||||
loadEntry() async {
|
return database.ready
|
||||||
try {
|
? NarrowView()
|
||||||
final value = await database.getEntries(limit: 1, dateISO: "");
|
: Center(
|
||||||
setState(() {
|
child: CircularProgressIndicator(
|
||||||
entry = value.documents[0];
|
value: database.progress,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String formatDate({String format = "", String? dateISO}) {
|
class NarrowView extends StatelessWidget {
|
||||||
final DateFormat dateFormatter = DateFormat(format);
|
const NarrowView({
|
||||||
final date = dateISO!.isEmpty ? DateTime.now() : DateTime.parse(dateISO);
|
super.key,
|
||||||
return dateFormatter.format(date);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return DefaultTabController(
|
||||||
child: entry == null
|
length: 4,
|
||||||
? CircularProgressIndicator()
|
initialIndex: 0,
|
||||||
: Column(
|
child: DayView()
|
||||||
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()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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:
|
environment:
|
||||||
sdk: '>=2.19.4 <4.0.0'
|
sdk: '>=2.19.4 <4.0.0'
|
||||||
@@ -18,6 +18,8 @@ dependencies:
|
|||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
pluto_grid: ^7.0.2
|
pluto_grid: ^7.0.2
|
||||||
string_to_hex: ^0.2.2
|
string_to_hex: ^0.2.2
|
||||||
|
flutter_form_builder: ^9.0.0
|
||||||
|
timezone: ^0.9.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user