Compare commits

..
23 Commits
Author SHA1 Message Date
ryan ececb7439a Fix invalid categories finally! 2024-01-19 01:44:25 -08:00
ryan fd171b8aa7 Update flake with direnv; add bashInteractive; fix creating new entries for the new year 2024-01-01 16:23:37 -05:00
ryan e01bd9895e Correctly build android app 2023-10-13 15:19:21 -07:00
ryan d7ecab3415 Clean up layout; fix colors and auto-populate 2023-10-13 12:53:31 -07:00
ryan 7b8cb39567 Small orion merge 2023-07-13 09:07:30 -07:00
ryan 8161b23755 Smashed the input bug! 2023-07-12 14:53:47 -07:00
ryan a7830aec27 Tiny layout edits 2023-07-08 18:03:50 -07:00
ryan b110ea0cb9 UI tweaks for Samsung 2023-07-08 16:26:02 -07:00
ryan 03382d2cb9 Fix updating category number from Realtime update 2023-06-26 14:10:44 -04:00
ryan 315a113f16 Fix small bug 2023-06-26 13:53:44 -04:00
ryan 9824429703 Fixed infinite-loading-new-entry bug. 2023-06-22 15:36:43 -04:00
ryan 2195a5324c Fixed? a key bug related to pulling the right day 2023-06-14 13:02:27 +04:00
ryan bbbe05ed3e dart fix 2023-06-08 02:51:47 -04:00
ryan e18abcdcb8 Fix category bug around "empty" category 2023-06-07 19:56:04 -04:00
ryan 5eb90ac1e1 Change name of android app. 2023-06-06 00:52:21 -04:00
ryan 66028b831d Bug fixes; Infinitypage; Android app 2023-06-06 00:33:40 -04:00
ryan 0ea14e9c02 Big update: can now update entries from flutter 2023-06-05 00:37:07 -04:00
ryan ca0fe7dd61 Realtime updating works! 2023-06-03 16:36:01 -04:00
ryan dfeb19126e Got a first draft of TodayPage working! 2023-06-03 00:21:09 -04:00
ryan 4f7d3879a8 dart fix 2023-06-02 01:03:01 -04:00
ryan 5dcfd5da8d Clean up account page 2023-06-01 20:18:38 -04:00
ryan 70aa37f3f5 Added ability to update categories 2023-06-01 16:08:06 -04:00
ryan c9c3f69886 Categories page works! 2023-06-01 00:52:44 -04:00
27 changed files with 1993 additions and 268 deletions
+28
View File
@@ -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"
}
]
}
+18
View File
@@ -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
View File
@@ -0,0 +1 @@
flake-profile-2-link
+1
View File
@@ -0,0 +1 @@
/nix/store/548c9rk983kzvkfiicqmkslqmd7s1ldg-nix-shell-env
+1
View File
@@ -0,0 +1 @@
/nix/store/rmm84806g3pgv1nc8wvi7g72ivh6axsi-nix-shell-env
+1 -1
View File
@@ -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 -->
+168
View File
@@ -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
}
+78
View File
@@ -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";
};
});
}
+24
View File
@@ -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();
}
}
+146 -19
View File
@@ -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}) {
+184 -16
View File
@@ -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
+33
View File
@@ -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;
}
}
+60
View File
@@ -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
View File
@@ -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),
],
)
],
),
),
);
}
}
+132
View File
@@ -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(),
),
),
],
),
);
}
}
+48 -2
View File
@@ -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"),
);
},
),
),
],
),
),
),
],
); );
} }
} }
+240 -35
View File
@@ -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,54 +11,258 @@ 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(
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: Center(
child: ListView.builder( child: Text(
itemCount: categories!.length + 1, style: TextStyle(
itemBuilder: (context, index) { fontWeight: FontWeight.bold,
if (index >= categories!.length || categories!.isEmpty) { color: category.foregroundColor,
return ListTile( fontSize: 18,
leading: Text("New Category"), ),
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 { } else {
Document? category = categories?[index]; api.updateCategory(
Color backgroundColor = name: name,
Color(int.parse("0x${category!.data['color']}")); number: double.parse(number),
Color textColor = backgroundColor.computeLuminance() > 0.2 color: color,
? Colors.black description: description,
: Colors.white; parentId: int.parse(parent),
return ListTile(
tileColor: backgroundColor,
textColor: textColor,
leading: Text(category.data['number'].toString()),
title: Text(category.data['name']),
); );
} }
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"),
),
],
),
), ),
); );
} }
+2 -1
View File
@@ -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 -1
View File
@@ -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';
+14 -4
View File
@@ -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(
+25 -75
View File
@@ -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) {
return database.ready
? NarrowView()
: Center(
child: CircularProgressIndicator(
value: database.progress,
));
});
} }
loadEntry() async {
try {
final value = await database.getEntries(limit: 1, dateISO: "");
setState(() {
entry = value.documents[0];
}); });
} 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 -1
View File
@@ -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: