1 Commits

Author SHA1 Message Date
64e2603542 Add publish workflow 2024-08-10 01:15:02 +02:00
44 changed files with 515 additions and 967 deletions

View File

@@ -0,0 +1,100 @@
# TODO: Need to install node here... How the fuck am I going to cache that bullshit...
# Figure out how to cache bullshit node
name: Release
on:
push:
tags:
- 'v*.*.*'
branches:
- master
jobs:
Publish:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /opt/hostedtoolcache
GOMODCACHE: /opt/hostedtoolcache/go/pkg/mod
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Get Go
uses: actions/setup-go@v3
with:
go-version-file: 'go.mod'
check-latest: true
cache: true
- name: Setup Wails
run: |
# Install Wails CLI (if needed)
go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: Build wails
run: wails build -platform darwin/amd64,darwin/arm64,windows/amd64,windows/arm64,linux/amd64,linux/arm64
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
Release notes for ${{ github.ref }}
- name: Upload Windows AMD64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-amd64.exe
asset_name: calorie-counter-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Windows ARM64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-arm64.exe
asset_name: calorie-counter-arm64.exe
asset_content_type: application/octet-stream
- name: Upload Linux AMD64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-amd64.exe
asset_name: calorie-counter-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Linux ARM64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-arm64.exe
asset_name: calorie-counter-arm64.exe
asset_content_type: application/octet-stream
- name: Upload Darwin AMD64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-amd64.exe
asset_name: calorie-counter-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Darwin ARM64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/bin/calorie-counter-arm64.exe
asset_name: calorie-counter-arm64.exe
asset_content_type: application/octet-stream

13
app.go
View File

@@ -2,8 +2,6 @@ package main
import ( import (
"context" "context"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
// App struct // App struct
@@ -45,12 +43,12 @@ func (a *App) UpdateFood(food Food) WailsFood1 {
return WailsFood1{Data: data, Success: true} return WailsFood1{Data: data, Success: true}
} }
func (a *App) GetLastPer100(name string) WailsFoodSearch { func (a *App) GetLastPer100(name string) WailsPer100 {
data, err := foodService.GetLastPer100(name) data, err := foodService.GetLastPer100(name)
if err != nil { if err != nil {
return WailsFoodSearch{Success: false, Error: err.Error()} return WailsPer100{Success: false, Error: err.Error()}
} }
return WailsFoodSearch{Data: data, Success: true} return WailsPer100{Data: data, Success: true}
} }
func (a *App) GetDailyFood() WailsAggregateFood { func (a *App) GetDailyFood() WailsAggregateFood {
@@ -137,8 +135,3 @@ func (a *App) SetSetting(key string, value int64) WailsGenericAck {
} }
return WailsGenericAck{Success: true} return WailsGenericAck{Success: true}
} }
//region other
func (a *App) Close() {
runtime.Quit(a.ctx)
}

34
db.go
View File

@@ -4,10 +4,9 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"os"
"time" "time"
"github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
type DB struct { type DB struct {
@@ -22,27 +21,7 @@ func (db *DB) Open() error {
return fmt.Errorf("database path not set") return fmt.Errorf("database path not set")
} }
file, err := os.Open(db.path) writeConn, err := sql.Open("sqlite3", db.path+"?_journal=WAL&_synchronous=NORMAL")
if err != nil {
if os.IsNotExist(err) {
log.Printf("Database file does not exist at %s, creating", db.path)
file, err := os.Create(db.path)
if err != nil {
return fmt.Errorf("failed to create database file: %v", err)
}
log.Printf("Database created at %s", db.path)
file.Close()
} else {
return fmt.Errorf("failed to open database file: %v", err)
}
}
file.Close()
sql.Register("spellfixlite", &sqlite3.SQLiteDriver{
Extensions: []string{"spellfix"},
})
writeConn, err := sql.Open("spellfixlite", db.path+"?_journal=WAL&_synchronous=NORMAL")
if err != nil { if err != nil {
Error.Printf("%++v", err) Error.Printf("%++v", err)
return err return err
@@ -52,7 +31,7 @@ func (db *DB) Open() error {
writeConn.SetConnMaxLifetime(30 * time.Second) writeConn.SetConnMaxLifetime(30 * time.Second)
db.writeConn = writeConn db.writeConn = writeConn
readConn, err := sql.Open("spellfixlite", db.path+"?mode=ro&_journal=WAL&_synchronous=NORMAL&_mode=ro") readConn, err := sql.Open("sqlite3", db.path+"?mode=ro&_journal=WAL&_synchronous=NORMAL&_mode=ro")
if err != nil { if err != nil {
Error.Printf("%++v", err) Error.Printf("%++v", err)
return err return err
@@ -100,7 +79,7 @@ func (db *DB) Init(ddl string) error {
if _, ok := rows[table]; !ok { if _, ok := rows[table]; !ok {
log.Printf("Table %s not found, initializing", table) log.Printf("Table %s not found, initializing", table)
needsInit = true needsInit = true
break break;
} }
} }
@@ -112,11 +91,6 @@ func (db *DB) Init(ddl string) error {
_, err = db.writeConn.Exec(ddl) _, err = db.writeConn.Exec(ddl)
if err != nil { if err != nil {
Error.Printf("%++v", err) Error.Printf("%++v", err)
log.Printf("%#v", "Rolling back")
_, err2 := db.writeConn.Exec("ROLLBACK;")
if err2 != nil {
Error.Printf("Error rollingback! %++v", err)
}
return err return err
} }

View File

@@ -1,7 +1,6 @@
begin; begin transaction;
create table weight ( create table weight (
id integer primary key,
date datetime default (datetime('now', '+2 hours')), date datetime default (datetime('now', '+2 hours')),
weight real not null weight real not null
); );
@@ -19,7 +18,7 @@ drop view if exists weightMonthly;
drop view if exists weightYearly; drop view if exists weightYearly;
create view weightView as create view weightView as
select id, select rowid,
date, date,
round(weight, 2) as weight round(weight, 2) as weight
from weight from weight
@@ -28,33 +27,32 @@ order by date desc;
create view weightDaily as create view weightDaily as
select strftime('%Y-%m-%d', date) as period, select strftime('%Y-%m-%d', date) as period,
round(avg(weight), 2) as amount round(avg(weight), 2) as amount
from weight from weight
group by strftime('%Y-%m-%d', date) group by strftime('%Y-%m-%d', date)
order by date desc; order by date desc;
create view weightWeekly as create view weightWeekly as
select strftime('%Y-%W', date) as period, select strftime('%Y-%W', date) as period,
round(avg(weight), 2) as amount round(avg(weight), 2) as amount
from weight from weight
group by strftime('%Y-%W', date) group by strftime('%Y-%W', date)
order by date desc; order by date desc;
create view weightMonthly as create view weightMonthly as
select strftime('%Y-%m', date) as period, select strftime('%Y-%m', date) as period,
round(avg(weight), 2) as amount round(avg(weight), 2) as amount
from weight from weight
group by strftime('%Y-%m', date) group by strftime('%Y-%m', date)
order by date desc; order by date desc;
create view weightYearly as create view weightYearly as
select strftime('%Y', date) as period, select strftime('%Y', date) as period,
round(avg(weight), 2) as amount round(avg(weight), 2) as amount
from weight from weight
group by strftime('%Y', date) group by strftime('%Y', date)
order by date desc; order by date desc;
create table food( create table food(
id integer primary key,
date datetime default (datetime('now', '+2 hours')), date datetime default (datetime('now', '+2 hours')),
food varchar not null, food varchar not null,
description varchar, description varchar,
@@ -63,80 +61,6 @@ create table food(
energy generated always as (coalesce(amount, 0) * coalesce(per100, 0) / 100) stored energy generated always as (coalesce(amount, 0) * coalesce(per100, 0) / 100) stored
); );
create virtual table foodfix using spellfix1;
insert into foodfix (word, rank)
select food as word,
count(*) as rank
from food
group by food;
drop trigger if exists food_foodfix_insert;
create trigger food_foodfix_insert AFTER
insert on food for EACH row
begin
update foodfix
set rank = rank + 1
where word = new.food;
insert into foodfix (word, rank)
select new.food,
1
where not exists (
select 1
from foodfix
where word = new.food
);
end;
drop trigger if exists food_foodfix_delete;
create trigger food_foodfix_delete AFTER
delete on food for EACH row
begin
update foodfix
set rank = rank - 1
where word = old.food;
delete from foodfix
where word = old.food
and rank <= 0;
end;
drop trigger if exists food_foodfix_update;
create trigger food_foodfix_update AFTER
update on food for EACH row
begin
update foodfix
set rank = rank - 1
where word = old.food;
delete from foodfix
where word = old.food
and rank <= 0;
update foodfix
set rank = rank + 1
where word = new.food;
insert into foodfix (word, rank)
select new.food,
1
where not exists (
select 1
from foodfix
where word = new.food
);
end;
with search_results as (
select word, score, f.rowid, f.*
from foodfix
inner join food f on f.food == word
where word match 'B'
)
select rowid, food, score, date, description, amount, per100, energy
from search_results
group by food
order by score asc, date desc;
create index dailyIdx on food(strftime('%Y-%m-%d', date)); create index dailyIdx on food(strftime('%Y-%m-%d', date));
create index weeklyIdx on food(strftime('%Y-%W', date)); create index weeklyIdx on food(strftime('%Y-%W', date));
create index monthlyIdx on food(strftime('%Y-%m', date)); create index monthlyIdx on food(strftime('%Y-%m', date));
@@ -152,7 +76,7 @@ drop view if exists foodYearly;
drop view if exists foodRecent; drop view if exists foodRecent;
create view foodView as create view foodView as
select id, select rowid,
date, date,
food, food,
description, description,
@@ -198,7 +122,8 @@ group by strftime('%Y', date)
order by date desc; order by date desc;
create view foodRecent as create view foodRecent as
select * select rowid,
*
from food from food
order by date desc order by date desc
limit 10; limit 10;
@@ -209,17 +134,17 @@ insert on food
begin begin
update food update food
set per100 = coalesce( set per100 = coalesce(
new.per100, new .per100,
( (
select per100 select per100
from food from food
where food = new.food where food = new .food
and per100 is not null and per100 is not null
order by date desc order by date desc
limit 1 limit 1
) )
) )
where id = new.id; where rowid = new .rowid;
end; end;
create table settings( create table settings(

View File

@@ -11,7 +11,7 @@ type (
db *DB db *DB
} }
Food struct { Food struct {
Id int64 `json:"id"` Rowid int64 `json:"rowid"`
Date string `json:"date"` Date string `json:"date"`
Food string `json:"food"` Food string `json:"food"`
Descripton string `json:"description"` Descripton string `json:"description"`
@@ -19,10 +19,6 @@ type (
Per100 float32 `json:"per100"` Per100 float32 `json:"per100"`
Energy float32 `json:"energy"` Energy float32 `json:"energy"`
} }
FoodSearch struct {
Food
Score int64 `json:"score"`
}
AggregatedFood struct { AggregatedFood struct {
Period string `json:"period"` Period string `json:"period"`
Amount float32 `json:"amount"` Amount float32 `json:"amount"`
@@ -31,11 +27,11 @@ type (
} }
) )
const foodColumns = "id, date, food, description, amount, per100, energy" const foodColumns = "rowid, date, food, description, amount, per100, energy"
const foodAggregatedColumns = "period, amount, avgPer100, energy" const foodAggregatedColumns = "period, amount, avgPer100, energy"
func (s *FoodService) GetRecent() ([]Food, error) { func (s *FoodService) GetRecent() ([]Food, error) {
var res []Food = []Food{} var res []Food
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get recent food, db is nil or is not ready") return res, fmt.Errorf("cannot get recent food, db is nil or is not ready")
} }
@@ -48,7 +44,7 @@ func (s *FoodService) GetRecent() ([]Food, error) {
for row.Next() { for row.Next() {
var food Food var food Food
err := row.Scan(&food.Id, &food.Date, &food.Food, &food.Descripton, &food.Amount, &food.Per100, &food.Energy) err := row.Scan(&food.Rowid, &food.Date, &food.Food, &food.Descripton, &food.Amount, &food.Per100, &food.Energy)
if err != nil { if err != nil {
log.Printf("error scanning row: %v", err) log.Printf("error scanning row: %v", err)
continue continue
@@ -60,48 +56,19 @@ func (s *FoodService) GetRecent() ([]Food, error) {
return res, nil return res, nil
} }
func (s *FoodService) GetLastPer100(name string) ([]FoodSearch, error) { func (s *FoodService) GetLastPer100(name string) (float32, error) {
res := []FoodSearch{}
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get last per100, db is nil or is not ready") return 0, fmt.Errorf("cannot get last per100, db is nil or is not ready")
} }
query := fmt.Sprintf(` row := s.db.readConn.QueryRow("SELECT per100 FROM food WHERE food like ? ORDER BY rowid DESC LIMIT 1", name+"%")
with search_results as ( var per100 float32
select word, score, f.rowid, f.*, err := row.Scan(&per100)
row_number() over (partition by f.food order by score asc, date desc) as rn
from foodfix
inner join food f on f.food == word
where word MATCH ?
)
select %s, score
from search_results
where rn = 1
order by score asc, date desc
limit %d
`, foodColumns, Settings.SearchLimit)
// log.Printf("%#v", query)
rows, err := s.db.readConn.Query(query, name)
if err != nil { if err != nil {
log.Printf("error getting last per100: %v", err) return 0, fmt.Errorf("error scanning row: %v", err)
return res, err
} }
for rows.Next() { return per100, nil
var f FoodSearch
err := rows.Scan(&f.Id, &f.Date, &f.Food.Food, &f.Descripton, &f.Amount, &f.Per100, &f.Energy, &f.Score)
if err != nil {
log.Printf("error scanning row: %v", err)
continue
}
res = append(res, f)
}
if len(res) == 0 {
return nil, fmt.Errorf("no results found for %s", name)
}
return res, nil
} }
func (s *FoodService) Create(food Food) (Food, error) { func (s *FoodService) Create(food Food) (Food, error) {
@@ -129,20 +96,20 @@ func (s *FoodService) Create(food Food) (Food, error) {
} }
} }
id, err := res.LastInsertId() rowid, err := res.LastInsertId()
if err != nil { if err != nil {
return food, fmt.Errorf("error getting last insert id: %v", err) return food, fmt.Errorf("error getting last insert id: %v", err)
} }
return s.GetById(id) return s.GetByRowid(rowid)
} }
func (s *FoodService) Update(food Food) (Food, error) { func (s *FoodService) Update(food Food) (Food, error) {
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return food, fmt.Errorf("cannot update food, db is nil or is not ready") return food, fmt.Errorf("cannot update food, db is nil or is not ready")
} }
if food.Id <= 0 { if food.Rowid <= 0 {
return food, fmt.Errorf("cannot update food, id is less than or equal to 0") return food, fmt.Errorf("cannot update food, rowid is less than or equal to 0")
} }
if food.Food == "" { if food.Food == "" {
return food, fmt.Errorf("cannot update food, food is empty") return food, fmt.Errorf("cannot update food, food is empty")
@@ -152,28 +119,28 @@ func (s *FoodService) Update(food Food) (Food, error) {
} }
if food.Per100 > 0 { if food.Per100 > 0 {
_, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ?, per100 = ? WHERE Id = ?", food.Food, food.Descripton, food.Amount, food.Per100, food.Id) _, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ?, per100 = ? WHERE rowid = ?", food.Food, food.Descripton, food.Amount, food.Per100, food.Rowid)
if err != nil { if err != nil {
return food, fmt.Errorf("error updating food: %v", err) return food, fmt.Errorf("error updating food: %v", err)
} }
} else { } else {
_, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ? WHERE Id = ?", food.Food, food.Descripton, food.Amount, food.Id) _, err := s.db.writeConn.Exec("UPDATE food SET food = ?, description = ?, amount = ? WHERE rowid = ?", food.Food, food.Descripton, food.Amount, food.Rowid)
if err != nil { if err != nil {
return food, fmt.Errorf("error updating food: %v", err) return food, fmt.Errorf("error updating food: %v", err)
} }
} }
return s.GetById(food.Id) return s.GetByRowid(food.Rowid)
} }
func (s *FoodService) GetById(id int64) (Food, error) { func (s *FoodService) GetByRowid(rowid int64) (Food, error) {
var res Food var res Food
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get food by id, db is nil or is not ready") return res, fmt.Errorf("cannot get food by rowid, db is nil or is not ready")
} }
row := s.db.readConn.QueryRow(fmt.Sprintf("SELECT %s from foodView WHERE id = ?", foodColumns), id) row := s.db.readConn.QueryRow(fmt.Sprintf("SELECT %s from foodView WHERE rowid = ?", foodColumns), rowid)
err := row.Scan(&res.Id, &res.Date, &res.Food, &res.Descripton, &res.Amount, &res.Per100, &res.Energy) err := row.Scan(&res.Rowid, &res.Date, &res.Food, &res.Descripton, &res.Amount, &res.Per100, &res.Energy)
if err != nil { if err != nil {
return res, fmt.Errorf("error scanning row: %v", err) return res, fmt.Errorf("error scanning row: %v", err)
} }
@@ -184,7 +151,7 @@ func (s *FoodService) GetById(id int64) (Food, error) {
// I could probably refactor this to be less of a disaster... // I could probably refactor this to be less of a disaster...
// But I think it'll work for now // But I think it'll work for now
func (s *FoodService) GetDaily() ([]AggregatedFood, error) { func (s *FoodService) GetDaily() ([]AggregatedFood, error) {
res := []AggregatedFood{} var res []AggregatedFood
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get daily food, db is nil or is not ready") return res, fmt.Errorf("cannot get daily food, db is nil or is not ready")
} }
@@ -210,7 +177,7 @@ func (s *FoodService) GetDaily() ([]AggregatedFood, error) {
} }
func (s *FoodService) GetWeekly() ([]AggregatedFood, error) { func (s *FoodService) GetWeekly() ([]AggregatedFood, error) {
res := []AggregatedFood{} var res []AggregatedFood
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get weekly food, db is nil or is not ready") return res, fmt.Errorf("cannot get weekly food, db is nil or is not ready")
} }
@@ -236,7 +203,7 @@ func (s *FoodService) GetWeekly() ([]AggregatedFood, error) {
} }
func (s *FoodService) GetMonthly() ([]AggregatedFood, error) { func (s *FoodService) GetMonthly() ([]AggregatedFood, error) {
res := []AggregatedFood{} var res []AggregatedFood
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get monthly food, db is nil or is not ready") return res, fmt.Errorf("cannot get monthly food, db is nil or is not ready")
} }
@@ -262,7 +229,7 @@ func (s *FoodService) GetMonthly() ([]AggregatedFood, error) {
} }
func (s *FoodService) GetYearly() ([]AggregatedFood, error) { func (s *FoodService) GetYearly() ([]AggregatedFood, error) {
res := []AggregatedFood{} var res []AggregatedFood
if s.db == nil || !s.db.Ready { if s.db == nil || !s.db.Ready {
return res, fmt.Errorf("cannot get yearly food, db is nil or is not ready") return res, fmt.Errorf("cannot get yearly food, db is nil or is not ready")
} }

View File

@@ -1,15 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport" /> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>calorie-counter</title> <title>calorie-counter</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="./src/main.ts" type="module"></script> <script src="./src/main.ts" type="module"></script>
</body> </body>
</html>
</html>

View File

@@ -10,10 +10,6 @@
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
"svelte": "^3.49.0", "svelte": "^3.49.0",
@@ -25,12 +21,15 @@
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^3.0.7" "vite": "^3.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"chart.js": "^4.4.3", "chart.js": "^4.4.3",
"regression": "^2.0.1",
"svelte-chartjs": "^3.1.5" "svelte-chartjs": "^3.1.5"
} }
} }

View File

@@ -1 +1 @@
23e2cc3e28f96f9d63ed35c0a34c1dcd bd9b801d4541f25052f0d4cb72b7d95a

View File

@@ -14,9 +14,6 @@ importers:
chart.js: chart.js:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
regression:
specifier: ^2.0.1
version: 2.0.1
svelte-chartjs: svelte-chartjs:
specifier: ^3.1.5 specifier: ^3.1.5
version: 3.1.5(chart.js@4.4.3)(svelte@3.59.2) version: 3.1.5(chart.js@4.4.3)(svelte@3.59.2)
@@ -717,9 +714,6 @@ packages:
resolution: {integrity: sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==} resolution: {integrity: sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==}
engines: {node: '>=8'} engines: {node: '>=8'}
regression@2.0.1:
resolution: {integrity: sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1518,8 +1512,6 @@ snapshots:
regexparam@2.0.2: {} regexparam@2.0.2: {}
regression@2.0.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve@1.22.8: resolve@1.22.8:

View File

@@ -1,32 +1,9 @@
<script lang="ts"> <script lang="ts">
import Header from "$lib/components/Header.svelte"; import Header from "$lib/components/Header.svelte";
import Router from "$lib/router/Router.svelte"; import Router from "$lib/router/Router.svelte";
import { Close } from "$wails/main/App"; import { Toaster } from 'svelte-sonner'
import { Toaster } from "svelte-sonner";
import * as srouter from "svelte-spa-router";
import { location } from "svelte-spa-router";
const energyLocRegex = /^\/(?:Energy)?(?!Weight)/;
const weightLocRegex = /^\/(?:Weight)(?!Energy)/;
function keyDown(event: KeyboardEvent) {
if (event.ctrlKey && event.key == "r") {
window.location.reload();
}
if (event.ctrlKey && event.key == "w") {
Close();
}
if (event.ctrlKey && event.key == "Tab") {
if (energyLocRegex.test($location)) {
srouter.replace($location.replace(energyLocRegex, "/Weight"));
} else if (weightLocRegex.test($location)) {
srouter.replace($location.replace(weightLocRegex, "/Energy"));
}
}
}
</script> </script>
<svelte:window on:keydown={keyDown} />
<Toaster /> <Toaster />
<template> <template>
<Header /> <Header />

View File

@@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import { main } from "$wails/models"; import { main } from "$wails/models";
export let item: main.AggregatedFood; export let item: main.AggregatedFood
</script> </script>
<template> <template>
<tr class="border-b border-gray-200 dark:border-gray-700"> <tr class="border-b border-gray-200 dark:border-gray-700">
<th <th class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800" scope="row">
scope="row"
>
{item.period} {item.period}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">

View File

@@ -3,7 +3,6 @@
import AggregatedFoodComp from "$lib/components/Energy/Aggregated/AggregatedFoodComp.svelte"; import AggregatedFoodComp from "$lib/components/Energy/Aggregated/AggregatedFoodComp.svelte";
import { main } from "$wails/models"; import { main } from "$wails/models";
import type { ChartData, Point } from "chart.js"; import type { ChartData, Point } from "chart.js";
import regression from "regression";
import { import {
CategoryScale, CategoryScale,
Chart as ChartJS, Chart as ChartJS,
@@ -14,7 +13,6 @@
Title, Title,
Tooltip, Tooltip,
} from "chart.js"; } from "chart.js";
import { CalculateR2 } from "$lib/utils";
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale); ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale);
@@ -24,18 +22,17 @@
let reversedItems = items.slice().reverse(); let reversedItems = items.slice().reverse();
const defaultOptions = { const defaultOptions = {
backgroundColor: "rgba(59, 130, 246, 0.1)", backgroundColor: "rgba(225, 204,230, .3)",
borderColor: "rgb(59, 130, 246)", borderColor: "rgb(205, 130, 158)",
pointBorderColor: "rgb(59, 130, 246)", pointBorderColor: "rgb(205, 130, 158)",
pointBackgroundColor: "rgb(255, 255, 255)", pointBackgroundColor: "rgb(255, 255, 255)",
pointBorderWidth: 2, pointBorderWidth: 10,
pointHoverRadius: 6, pointHoverRadius: 5,
pointHoverBackgroundColor: "rgb(59, 130, 246)", pointHoverBackgroundColor: "rgb(0, 157, 123)",
pointHoverBorderColor: "rgb(255, 255, 255)", pointHoverBorderColor: "rgba(220, 220, 220, 1)",
pointHoverBorderWidth: 2, pointHoverBorderWidth: 2,
pointRadius: 3, pointRadius: 1,
pointHitRadius: 20, pointHitRadius: 10,
tension: 0.4,
}; };
const data: ChartData<"line", (number | Point)[]> = { const data: ChartData<"line", (number | Point)[]> = {
labels: reversedItems.map((f) => f.period), labels: reversedItems.map((f) => f.period),
@@ -77,83 +74,28 @@
}, },
], ],
}; };
data.datasets.push({
...defaultOptions,
label: "R2",
data: CalculateR2(reversedItems.map((f, i) => [i, f.energy])),
borderColor: "#04d1d1",
pointBorderColor: "#04d1d1",
pointRadius: 0,
});
</script> </script>
<template> <template>
<div class="flex flex-col gap-6 p-6 bg-gray-900 min-h-screen"> <Line {data} class="max-h-[50vh] h-[50vh]" />
<div class="bg-gray-800 rounded-lg p-6 shadow-lg border border-gray-700"> <div class="relative flex flex-col h-[43vh]" data-vaul-drawer-wrapper id="page">
<Line <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
{data} <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
options={{ <thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
responsive: true, <tr>
maintainAspectRatio: false, <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-2/12" scope="col"> Period </th>
plugins: { <th class="px-6 py-3" scope="col"> Amount </th>
legend: { <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800" scope="col"> AvgPer100 </th>
position: "top", <th class="px-6 py-3" scope="col"> Energy </th>
labels: { </tr>
padding: 20, </thead>
color: "rgb(229, 231, 235)", <tbody>
font: { {#each items as f}
size: 12, <AggregatedFoodComp item={f} />
family: "'Nunito', sans-serif", {/each}
}, </tbody>
}, </table>
},
},
scales: {
y: {
grid: {
color: "rgba(75, 85, 99, 0.2)",
},
ticks: {
color: "rgb(229, 231, 235)",
font: {
family: "'Nunito', sans-serif",
},
},
},
x: {
grid: {
color: "rgba(75, 85, 99, 0.2)",
},
ticks: {
color: "rgb(229, 231, 235)",
font: {
family: "'Nunito', sans-serif",
},
},
},
},
}}
class="max-h-[50vh] h-[50vh]"
/>
</div>
<div class="relative flex flex-col h-[43vh]" data-vaul-drawer-wrapper id="page">
<div class="relative overflow-x-auto shadow-md rounded-lg">
<table class="w-full text-sm text-left text-gray-400">
<thead class="text-xs uppercase bg-gray-800 text-gray-200 sticky top-0">
<tr>
<th class="px-6 py-4 font-semibold" scope="col">Period</th>
<th class="px-6 py-4 font-semibold" scope="col">Amount</th>
<th class="px-6 py-4 font-semibold" scope="col">AvgPer100</th>
<th class="px-6 py-4 font-semibold" scope="col">Energy</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
{#each items as f}
<AggregatedFoodComp item={f} />
{/each}
</tbody>
</table>
</div>
</div> </div>
<div></div>
</div> </div>
</template> </template>

View File

@@ -3,13 +3,12 @@
import { main } from "$wails/models"; import { main } from "$wails/models";
import { CreateFood, GetLastPer100 } from "$wails/main/App"; import { CreateFood, GetLastPer100 } from "$wails/main/App";
import { foodStore } from "$lib/store/Energy/foodStore"; import { foodStore } from "$lib/store/Energy/foodStore";
import FoodSearchEntry from "./FoodSearchEntry.svelte";
let item: main.Food = { let item: main.Food = {
food: "", food: "",
amount: 0, amount: 0,
description: "", description: "",
id: 0, rowid: 0,
date: "", date: "",
per100: 0, per100: 0,
energy: 0, energy: 0,
@@ -20,53 +19,6 @@
let per100: string = ""; let per100: string = "";
let per100Edited: boolean = false; let per100Edited: boolean = false;
let per100Element: HTMLTableCellElement; let per100Element: HTMLTableCellElement;
let nameElement: HTMLTableCellElement;
let autocompleteList: HTMLUListElement;
let foodSearch: main.Food[] = [];
let hiLiteIndex: number = -1;
$: {
name = name.trim();
if (!name) {
foodSearch = [];
} else {
updateAutocomplete();
}
}
function updateAutocomplete() {
if (!per100Edited)
GetLastPer100(name.trim()).then((res) => {
if (res.success && res.data && name) {
foodSearch = res.data;
hiLiteIndex = -1;
} else {
foodSearch = [];
}
});
}
async function handleSubmit() {
item.food = name;
item.description = description;
item.amount = parseInt(amount);
item.per100 = parseInt(per100);
const res = await CreateFood(item);
name = "";
amount = "";
per100 = "";
per100Edited = false;
if (!res.success) {
toast.error(`failed to create item with error ${res.error}`);
return;
}
foodStore.update((value) => [res.data, ...value]);
nameElement.focus();
foodSearch = [];
}
async function update(event: KeyboardEvent & { currentTarget: EventTarget & HTMLTableCellElement }) { async function update(event: KeyboardEvent & { currentTarget: EventTarget & HTMLTableCellElement }) {
name = name.trim(); name = name.trim();
@@ -74,103 +26,64 @@
description = description.trim(); description = description.trim();
per100 = per100.trim(); per100 = per100.trim();
if (!name) { if (!per100Edited && event.currentTarget === per100Element) per100Edited = true;
foodSearch = [];
return;
}
if (!per100Edited && event.currentTarget == per100Element) per100Edited = true; if (event.key == "Enter") {
if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
item.food = name;
item.description = description;
item.amount = parseInt(amount);
item.per100 = parseInt(per100);
// If suggestions are visible and we have a highlighted item or at least one suggestion const res = await CreateFood(item);
if (foodSearch.length > 0) { name = "";
const selectedFood = hiLiteIndex >= 0 ? foodSearch[hiLiteIndex] : foodSearch[0]; amount = "";
setInputVal(selectedFood); // description = ''
per100 = "";
per100Edited = false;
if (!res.success) {
toast.error(`failed to create item with error ${res.error}`);
return; return;
} }
// Only submit if we have no suggestions visible foodStore.update((value) => [res.data, ...value]);
if (name && amount && per100) {
await handleSubmit();
}
} }
}
function navigateList(e: KeyboardEvent) { if (!per100Edited)
if (!foodSearch.length) return; GetLastPer100(name.trim()).then((res) => {
if (res.success) {
switch (e.key) { per100 = res.data.toString();
case "ArrowDown": }
e.preventDefault(); });
hiLiteIndex = Math.min(hiLiteIndex + 1, foodSearch.length - 1);
break;
case "ArrowUp":
e.preventDefault();
hiLiteIndex = Math.max(hiLiteIndex - 1, -1);
break;
case "Escape":
e.preventDefault();
foodSearch = [];
hiLiteIndex = -1;
break;
}
}
function setInputVal(food: main.Food) {
name = food.food;
per100 = String(food.per100);
amount = String(food.amount);
hiLiteIndex = -1;
foodSearch = [];
}
$: {
if (nameElement && autocompleteList) {
const { top, left, height } = nameElement.getBoundingClientRect();
autocompleteList.style.top = `${top + height}px`;
autocompleteList.style.left = `${left}px`;
}
}
let timeout: number;
function handleFocusOut() {
timeout = setTimeout(() => {
foodSearch = [];
hiLiteIndex = -1;
}, 100);
}
function handleFocusIn() {
clearTimeout(timeout);
updateAutocomplete();
} }
</script> </script>
<svelte:window on:keydown={navigateList} />
<template> <template>
<tr class="border-b border-gray-700 text-lg font-medium hover:bg-gray-800/50 transition-colors"> <tr class="border-b border-gray-200 dark:border-gray-700 text-lg font-bold">
<th class="px-6 py-4 font-medium text-gray-200 whitespace-nowrap" scope="row" /> <th
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
scope="row"
>
</th>
<!-- svelte-ignore a11y-autofocus -->
<td <td
bind:innerText={name} bind:innerText={name}
class="px-6 py-4 overflow-hidden focus:outline-none focus:ring-2 focus:ring-blue-500 rounded transition-all" class:border-[3px]={!name}
class:ring-2={!name} class:border-red-600={!name}
class:ring-red-500={!name} class="px-6 py-4 overflow-hidden"
contenteditable="true" contenteditable="true"
autofocus autofocus
on:keydown={update} on:keydown={update}
on:focusin={handleFocusIn} >
on:focusout={handleFocusOut} </td>
bind:this={nameElement}
/>
<td <td
bind:innerText={description} bind:innerText={description}
class="px-6 py-4 bg-gray-50 dark:bg-gray-800 overflow-hidden" class="px-6 py-4 bg-gray-50 dark:bg-gray-800 overflow-hidden"
contenteditable="true" contenteditable="true"
on:keydown={update} on:keydown={update}
/> >
</td>
<td <td
bind:innerText={amount} bind:innerText={amount}
class:border-[3px]={!amount} class:border-[3px]={!amount}
@@ -178,7 +91,8 @@
class="px-6 py-4 overflow-hidden" class="px-6 py-4 overflow-hidden"
contenteditable="true" contenteditable="true"
on:keydown={update} on:keydown={update}
/> >
</td>
<td <td
bind:this={per100Element} bind:this={per100Element}
bind:innerText={per100} bind:innerText={per100}
@@ -187,13 +101,7 @@
class:border-orange-600={!per100} class:border-orange-600={!per100}
contenteditable="true" contenteditable="true"
on:keydown={update} on:keydown={update}
/> >
</td>
</tr> </tr>
{#if foodSearch.length > 0}
<ul bind:this={autocompleteList} class="z-50 fixed top-0 left-0 w-3/12 border border-x-gray-800">
{#each foodSearch as f, i}
<FoodSearchEntry itemLabel={f.food} highlighted={i === hiLiteIndex} on:click={() => setInputVal(f)} />
{/each}
</ul>
{/if}
</template> </template>

View File

@@ -34,18 +34,16 @@
remainingToday = $settingsStore.target; remainingToday = $settingsStore.target;
let now = new Date(); let now = new Date();
let todayDate = formatter.format(now); let todayDate = formatter.format(now);
const [day, month, year] = todayDate.split("/"); const [day, month, year] = todayDate.split('/');
todayDate = `${year}-${month}-${day}`; todayDate = `${year}-${month}-${day}`;
if ($foodStore) { $foodStore.forEach((food) => {
$foodStore.forEach((food) => { if (food.date.split("T")[0] == todayDate) {
if (food.date.split("T")[0] == todayDate) { remainingToday -= food.energy;
remainingToday -= food.energy; } else {
} else { return;
return; }
} });
});
}
remainingToday = Math.round(remainingToday); remainingToday = Math.round(remainingToday);
computeColor(); computeColor();
} }
@@ -57,11 +55,5 @@
</script> </script>
<template> <template>
<div <div class="px-4 text-bold text-3xl" style="color: {color}">{remainingToday}</div>
class="px-6 py-3 text-3xl font-bold rounded-lg bg-gray-800/50 shadow-lg border border-gray-700"
style="color: {color}"
>
<span class="mr-2">{remainingToday}</span>
<span class="text-lg text-gray-400">kcal remaining</span>
</div>
</template> </template>

View File

@@ -1,94 +1,84 @@
<script lang="ts"> <script lang="ts">
import { UpdateFood } from "$wails/main/App"; import { UpdateFood } from '$wails/main/App';
import { main } from "$wails/models"; import {main} from '$wails/models'
import { toast } from "svelte-sonner"; import { toast } from 'svelte-sonner'
export let item: main.Food; export let item: main.Food
export let energyColor: string; export let energyColor: string
export let nameColor: string; export let nameColor: string
export let dateColor: string; export let dateColor: string
let amount: string = item.amount.toString(); let amount: string = item.amount.toString()
let per100: string = item.per100?.toString() ?? ""; let per100: string = item.per100?.toString() ?? ''
let description: string = item.description ?? ""; let description: string = item.description ?? ''
let name: string = item.food; let name: string = item.food
async function update(event: KeyboardEvent & { currentTarget: EventTarget & HTMLTableCellElement }) { async function update(event: KeyboardEvent & { currentTarget: (EventTarget & HTMLTableCellElement) }) {
if (event.key == "Enter") { if (event.key == 'Enter') {
event.preventDefault(); event.preventDefault()
await updateItem(); await updateItem()
} }
} }
async function focusOutUpdate() { async function focusOutUpdate() {
await updateItem(); await updateItem()
} }
async function updateItem() { async function updateItem() {
amount = amount.trim(); amount = amount.trim()
per100 = per100.trim(); per100 = per100.trim()
description = description.trim(); description = description.trim()
name = name.trim(); name = name.trim()
item.food = name; item.food = name
item.description = description; item.description = description
item.amount = parseInt(amount); item.amount = parseInt(amount)
item.per100 = parseInt(per100); item.per100 = parseInt(per100)
const res = await UpdateFood(item); const res = await UpdateFood(item)
if (!res.success) { if (!res.success) {
toast.error(`failed to update food item with error ${res.error}`); toast.error(`failed to update food item with error ${res.error}`)
} else { } else {
item = res.data; item = res.data
} }
name = item.food; name = item.food
description = item.description ?? ""; description = item.description ?? ''
amount = item.amount.toString(); amount = item.amount.toString()
per100 = item.per100?.toString() ?? ""; per100 = item.per100?.toString() ?? ''
} }
</script> </script>
<template> <template>
<tr class="border-b border-gray-200 dark:border-gray-700 font-bold text-lg"> <tr class="border-b border-gray-200 dark:border-gray-700 font-bold text-lg">
<th <th class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800" style="color: {dateColor}"
style="color: {dateColor}" scope="row">
scope="row"
>
{item.date} {item.date}
</th> </th>
<td <td class="px-6 py-4"
class="px-6 py-4" style="color: {nameColor}"
style="color: {nameColor}" contenteditable="true"
contenteditable="true" bind:innerText={name}
bind:innerText={name} on:focusout={focusOutUpdate}
on:focusout={focusOutUpdate} on:keydown={update}>
on:keydown={update}
>
</td> </td>
<td <td class="px-6 py-4 bg-gray-50 dark:bg-gray-800"
class="px-6 py-4 bg-gray-50 dark:bg-gray-800" contenteditable="true"
contenteditable="true" bind:innerText={description}
bind:innerText={description} on:focusout={focusOutUpdate}
on:focusout={focusOutUpdate} on:keydown={update}>
on:keydown={update}
>
</td> </td>
<td <td class="px-6 py-4"
class="px-6 py-4" contenteditable="true"
contenteditable="true" bind:innerText={amount}
bind:innerText={amount} on:focusout={focusOutUpdate}
on:focusout={focusOutUpdate} on:keydown={update}>
on:keydown={update}
>
</td> </td>
<td <td class="px-6 py-4 bg-gray-50 dark:bg-gray-800"
class="px-6 py-4 bg-gray-50 dark:bg-gray-800" contenteditable="true"
contenteditable="true" bind:innerText={per100}
bind:innerText={per100} on:focusout={focusOutUpdate}
on:focusout={focusOutUpdate} on:keydown={update}>
on:keydown={update}
>
</td> </td>
<td class="px-6 py-4" style="color: {energyColor}"> <td class="px-6 py-4" style="color: {energyColor}">
{item.energy} {item.energy}

View File

@@ -1,13 +0,0 @@
<script lang="ts">
export let itemLabel: string;
export let highlighted: boolean;
</script>
<li
class="list-none px-4 py-3 cursor-pointer bg-gray-800 text-gray-200 text-base border-b border-gray-700 transition-colors"
class:bg-blue-600={highlighted}
class:text-white={highlighted}
on:click
>
{itemLabel}
</li>

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { GenerateColor, RemoveExistingColors } from "$lib/utils"; import { GenerateColor } from "$lib/utils";
import { main } from "$wails/models"; import { main } from "$wails/models";
import EmptyFoodComp from "./EmptyFoodComp.svelte"; import EmptyFoodComp from "./EmptyFoodComp.svelte";
import FoodComp from "./FoodComp.svelte"; import FoodComp from "./FoodComp.svelte";
export let items: main.Food[] = []; export let items: main.Food[] = [];
RemoveExistingColors();
let minCal = 1e5; let minCal = 1e5;
let maxCal = 0; let maxCal = 0;
@@ -59,20 +58,20 @@
</script> </script>
<template> <template>
<div class="relative flex flex-col flex-grow h-[93vh] select-none bg-gray-900" data-vaul-drawer-wrapper id="page"> <div class="relative flex flex-col flex-grow h-[93vh] select-none" data-vaul-drawer-wrapper id="page">
<div class="relative overflow-auto h-full shadow-md rounded-lg"> <div class="relative overflow-auto h-full shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-400"> <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-800 text-gray-200 sticky top-0"> <thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
<tr> <tr>
<th class="px-6 py-4 font-semibold w-2/12" scope="col">Date</th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-2/12" scope="col"> Date </th>
<th class="px-6 py-4 font-semibold" scope="col">Food</th> <th class="px-6 py-3 w-3/12" scope="col"> Food </th>
<th class="px-6 py-4 font-semibold" scope="col">Description</th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-4/12" scope="col"> Description </th>
<th class="px-6 py-4 font-semibold" scope="col">Amount</th> <th class="px-6 py-3 w-1/12" scope="col"> Amount </th>
<th class="px-6 py-4 font-semibold" scope="col">Cal Per 100</th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-1/12" scope="col"> Cal Per 100 </th>
<th class="px-6 py-4 font-semibold" scope="col">Energy</th> <th class="px-6 py-3 w-1/12" scope="col"> Energy </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-700"> <tbody>
<EmptyFoodComp /> <EmptyFoodComp />
{#each items as f} {#each items as f}
<FoodComp <FoodComp
@@ -85,5 +84,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div></div>
</div> </div>
</template> </template>

View File

@@ -4,7 +4,6 @@
import Fa from "svelte-fa"; import Fa from "svelte-fa";
import Settings from "./Settings/Settings.svelte"; import Settings from "./Settings/Settings.svelte";
import EnergyToday from "./Energy/EnergyToday.svelte"; import EnergyToday from "./Energy/EnergyToday.svelte";
import RefreshComponent from "./RefreshComponent.svelte";
Fa; Fa;
type Link = { type Link = {
@@ -49,40 +48,47 @@
</script> </script>
<header <header
class="flex h-24 items-center justify-between bg-gray-900 shadow-lg sticky top-0 z-50 border-b border-gray-700/40 backdrop-blur supports-[backdrop-filter]:bg-gray-900/60 px-6" class="flex h-22 items-center justify-center bg-base-100 shadow-lg sticky top-0 z-50 border-b
border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
> >
<div class="flex flex-col gap-2"> <div>
<nav class="flex space-x-6 text-2xl font-bold select-none"> <nav class="flex space-x-4 text-2xl font-bold select-none justify-center">
<a <a
use:link use:link
class="transition-colors hover:text-gray-200 {$location === '/' ? 'text-white' : 'text-gray-400'}" class={"/" == $location
? "transition-colors hover:text-foreground/80"
: "transition-colors hover:text-foreground/80 text-foreground/60"}
href="/" href="/"
on:click={() => (selected = "Energy")}>Energy</a on:click={(e) => (selected = "Energy")}>Energy</a
> >
<a <a
use:link use:link
class="transition-colors hover:text-gray-200 {$location === '/Weight' ? 'text-white' : 'text-gray-400'}" class={"/Weight" == $location
? "transition-colors hover:text-foreground/80"
: "transition-colors hover:text-foreground/80 text-foreground/60"}
href="/Weight" href="/Weight"
on:click={() => (selected = "Weight")}>Weight</a on:click={(e) => (selected = "Weight")}>Weight</a
> >
</nav> </nav>
<nav class="flex space-x-6 text-xl font-medium select-none"> <nav class="flex space-x-4 text-2xl font-bold select-none justify-center">
{#each sublinks as { label, href }} {#each sublinks as { label, href }}
<a <a
use:link use:link
{href} {href}
class="transition-colors hover:text-gray-200 {href === $location ? 'text-white' : 'text-gray-400'}" class={href == $location
>{label}</a ? "transition-colors hover:text-foreground/80"
: "transition-colors hover:text-foreground/80 text-foreground/60"}>{label}</a
> >
{/each} {/each}
</nav> </nav>
</div> </div>
<div class="flex items-center gap-6"> <!-- svelte-ignore a11y-click-events-have-key-events -->
<EnergyToday /> <div class="absolute right-0 pt-4 pb-4 pr-8 pl-8 cursor-pointer" on:click={() => (showModal = true)}>
<button class="p-3 hover:bg-gray-800 rounded-lg transition-colors" on:click={() => (showModal = true)}> <button>
<Fa icon={faGear} scale={2} /> <Fa icon={faGear} scale={2} />
</button> </button>
</div> </div>
<EnergyToday />
</header> </header>
<Settings bind:showModal /> <Settings bind:showModal />

View File

@@ -1,42 +0,0 @@
<script lang="ts">
import { dailyFoodStore } from "$lib/store/Energy/dailyFoodStore";
import { monthlyFoodStore } from "$lib/store/Energy/monthlyFoodStore";
import { weeklyFoodStore } from "$lib/store/Energy/weeklyFoodStore";
import { yearlyFoodStore } from "$lib/store/Energy/yearlyFoodStore";
import { dailyWeightStore } from "$lib/store/Weight/dailyWeightStore";
import { monthlyWeightStore } from "$lib/store/Weight/monthlyWeightStore";
import { weeklyWeightStore } from "$lib/store/Weight/weeklyWeightStore";
import { yearlyWeightStore } from "$lib/store/Weight/yearlyWeightStore";
import { faRefresh } from "@fortawesome/free-solid-svg-icons";
import Fa from "svelte-fa";
Fa;
// YES refresh DOES exist FFS
function refreshStores() {
// @ts-ignore
dailyFoodStore.refresh();
// @ts-ignore
weeklyFoodStore.refresh();
// @ts-ignore
monthlyFoodStore.refresh();
// @ts-ignore
yearlyFoodStore.refresh();
// @ts-ignore
dailyWeightStore.refresh();
// @ts-ignore
weeklyWeightStore.refresh();
// @ts-ignore
monthlyWeightStore.refresh();
// @ts-ignore
yearlyWeightStore.refresh();
}
</script>
<template>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="absolute right-20 pt-4 pb-4 pr-8 pl-8 cursor-pointer" on:click={refreshStores}>
<button class="p-3 hover:bg-gray-800 rounded-lg transition-colors">
<Fa icon={faRefresh} scale={2} />
</button>
</div>
</template>

View File

@@ -18,7 +18,7 @@
if (!res.success) { if (!res.success) {
toast.error(`Failed to set setting with error ${res.error}`); toast.error(`Failed to set setting with error ${res.error}`);
editSetting = String(setting); editSetting = String(setting);
return; return
} }
setting = numSetting; setting = numSetting;
settingsStore.update((store) => { settingsStore.update((store) => {

View File

@@ -11,7 +11,7 @@
<div class="flex flex-2 flex-col gap-4 text-left text-xl"> <div class="flex flex-2 flex-col gap-4 text-left text-xl">
{#each Object.keys($settingsStore) as key} {#each Object.keys($settingsStore) as key}
<Setting {key} setting={$settingsStore[key]} /> <Setting key={key} setting={$settingsStore[key]} />
{/each} {/each}
</div> </div>
</Modal> </Modal>

View File

@@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import { main } from "$wails/models"; import {main} from '$wails/models'
export let item: main.AggregatedWeight; export let item: main.AggregatedWeight
</script> </script>
<template> <template>
<tr class="border-b border-gray-200 dark:border-gray-700"> <tr class="border-b border-gray-200 dark:border-gray-700">
<th <th class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800" scope="row">
scope="row"
>
{item.period} {item.period}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">

View File

@@ -13,7 +13,6 @@
Title, Title,
Tooltip, Tooltip,
} from "chart.js"; } from "chart.js";
import { CalculateR2 } from "$lib/utils";
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale); ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale);
@@ -21,18 +20,17 @@
let reversedItems = items.slice().reverse(); let reversedItems = items.slice().reverse();
const defaultOptions = { const defaultOptions = {
backgroundColor: "rgba(59, 130, 246, 0.1)", backgroundColor: "rgba(225, 204,230, .3)",
borderColor: "rgb(59, 130, 246)", borderColor: "rgb(205, 130, 158)",
pointBorderColor: "rgb(59, 130, 246)", pointBorderColor: "rgb(205, 130, 158)",
pointBackgroundColor: "rgb(255, 255, 255)", pointBackgroundColor: "rgb(255, 255, 255)",
pointBorderWidth: 2, pointBorderWidth: 10,
pointHoverRadius: 6, pointHoverRadius: 5,
pointHoverBackgroundColor: "rgb(59, 130, 246)", pointHoverBackgroundColor: "rgb(0, 157, 123)",
pointHoverBorderColor: "rgb(255, 255, 255)", pointHoverBorderColor: "rgba(220, 220, 220, 1)",
pointHoverBorderWidth: 2, pointHoverBorderWidth: 2,
pointRadius: 3, pointRadius: 1,
pointHitRadius: 20, pointHitRadius: 10,
tension: 0.4,
}; };
const data: ChartData<"line", (number | Point)[]> = { const data: ChartData<"line", (number | Point)[]> = {
labels: reversedItems.map((f) => f.period), labels: reversedItems.map((f) => f.period),
@@ -46,81 +44,26 @@
}, },
], ],
}; };
data.datasets.push({
...defaultOptions,
label: "R2",
data: CalculateR2(reversedItems.map((f, i) => [i, f.amount])),
borderColor: "#04d1d1",
pointBorderColor: "#04d1d1",
pointRadius: 0,
});
</script> </script>
<template> <template>
<div class="flex flex-col gap-6 p-6 bg-gray-900 min-h-screen"> <Line {data} class="max-h-[50vh] h-[50vh]" />
<div class="bg-gray-800 rounded-lg p-6 shadow-lg border border-gray-700"> <div class="relative flex flex-col h-[43vh]" data-vaul-drawer-wrapper id="page">
<Line <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
{data} <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
options={{ <thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
responsive: true, <tr>
maintainAspectRatio: false, <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-2/12" scope="col"> Period </th>
plugins: { <th class="px-6 py-3" scope="col"> Amount </th>
legend: { </tr>
position: "top", </thead>
labels: { <tbody>
padding: 20, {#each items as f}
color: "rgb(229, 231, 235)", <AggregatedWeightComp item={f} />
font: { {/each}
size: 12, </tbody>
family: "'Nunito', sans-serif", </table>
},
},
},
},
scales: {
y: {
grid: {
color: "rgba(75, 85, 99, 0.2)",
},
ticks: {
color: "rgb(229, 231, 235)",
font: {
family: "'Nunito', sans-serif",
},
},
},
x: {
grid: {
color: "rgba(75, 85, 99, 0.2)",
},
ticks: {
color: "rgb(229, 231, 235)",
font: {
family: "'Nunito', sans-serif",
},
},
},
},
}}
class="max-h-[50vh] h-[50vh]"
/>
</div>
<div class="relative flex flex-col h-[43vh]" data-vaul-drawer-wrapper id="page">
<div class="relative overflow-x-auto shadow-md rounded-lg">
<table class="w-full text-sm text-left text-gray-400">
<thead class="text-xs uppercase bg-gray-800 text-gray-200 sticky top-0">
<tr>
<th class="px-6 py-4 font-semibold" scope="col">Period</th>
<th class="px-6 py-4 font-semibold" scope="col">Amount</th>
</tr>
</thead>
<tbody>
{#each items as f}
<AggregatedWeightComp item={f} />
{/each}
</tbody>
</table>
</div>
</div> </div>
<div></div>
</div> </div>
</template> </template>

View File

@@ -1,34 +1,34 @@
<script lang="ts"> <script lang="ts">
import { toast } from "svelte-sonner"; import { toast } from 'svelte-sonner'
import { main } from "$wails/models"; import { main } from "$wails/models";
import { CreateWeight } from "$wails/main/App"; import { CreateWeight } from '$wails/main/App';
import { weightStore } from "$lib/store/Weight/weightStore"; import { weightStore } from '$lib/store/Weight/weightStore';
let item: main.Weight = { let item: main.Weight = {
weight: 0, weight: 0,
id: 0, rowid: 0,
date: "", date: ''
}; }
let weight: string | null = null; let weight: string | null = null
async function update(event: KeyboardEvent & { currentTarget: EventTarget & HTMLTableCellElement }) { async function update(event: KeyboardEvent & { currentTarget: (EventTarget & HTMLTableCellElement) }) {
if (!weight) return; if (!weight) return
weight = weight.trim(); weight = weight.trim()
if (event.key == "Enter") { if (event.key == 'Enter') {
event.preventDefault(); event.preventDefault()
item.weight = parseFloat(weight); item.weight = parseFloat(weight)
if (isNaN(item.weight)) { if (isNaN(item.weight)) {
toast.error("Weight must be a number"); toast.error('Weight must be a number')
return; return
} }
const res = await CreateWeight(item); const res = await CreateWeight(item)
weight = ""; weight = ''
if (!res.success) { if (!res.success) {
toast.error(`failed to create weight with error ${res.error}`); toast.error(`failed to create weight with error ${res.error}`)
return; return
} }
console.log("update"); console.log("update");
@@ -39,20 +39,16 @@
<template> <template>
<tr class="border-b border-gray-200 dark:border-gray-700 text-lg font-bold"> <tr class="border-b border-gray-200 dark:border-gray-700 text-lg font-bold">
<th <th class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800" scope="row">
scope="row"
>
</th> </th>
<td <td bind:innerText={weight}
bind:innerText={weight} class:border-[3px]={!weight}
class:border-[3px]={!weight} class:border-red-600={!weight}
class:border-red-600={!weight} class="px-6 py-4 overflow-hidden"
class="px-6 py-4 overflow-hidden" contenteditable="true"
contenteditable="true" autofocus
autofocus on:keydown={update}>
on:keydown={update}
>
</td> </td>
</tr> </tr>
</template> </template>

View File

@@ -1,17 +1,15 @@
<script lang="ts"> <script lang="ts">
import { main } from "$wails/models"; import { main } from "$wails/models";
export let item: main.Weight; export let item: main.Weight
export let dateColor: string; export let dateColor: string
</script> </script>
<template> <template>
<tr class="border-b border-gray-200 dark:border-gray-700 font-bold text-lg"> <tr class="border-b border-gray-200 dark:border-gray-700 font-bold text-lg">
<th <th class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800" style="color: {dateColor}"
style="color: {dateColor}" scope="row">
scope="row"
>
{item.date} {item.date}
</th> </th>
<td class="px-6 py-4"> <td class="px-6 py-4">

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { GenerateColor, RemoveExistingColors } from "$lib/utils"; import { GenerateColor } from "$lib/utils";
import EmptyWeightComp from "$components/Weight/EmptyWeightComp.svelte"; import EmptyWeightComp from "$components/Weight/EmptyWeightComp.svelte";
import WeightComp from "$components/Weight/WeightComp.svelte"; import WeightComp from "$components/Weight/WeightComp.svelte";
import { main } from "$wails/models"; import { main } from "$wails/models";
export let items: main.Weight[] = []; export let items: main.Weight[] = [];
RemoveExistingColors();
const dateColors: Map<string, string> = new Map<string, string>(); const dateColors: Map<string, string> = new Map<string, string>();
@@ -15,22 +14,25 @@
const date = item.date.toString().split("T")[0]; const date = item.date.toString().split("T")[0];
if (!date) return GenerateColor(); if (!date) return GenerateColor();
if (!dateColors.has(date)) dateColors.set(date, GenerateColor()); if (!dateColors.has(date)) dateColors.set(date, GenerateColor());
// THERE'S NOTHING UNDEFINED HERE
// WE GOT RID OF UNDEFINED ON LINE 33
// ASSHOLE
// @ts-ignore // @ts-ignore
return dateColors.get(date); return dateColors.get(date);
} }
</script> </script>
<template> <template>
<div class="relative flex flex-col flex-grow h-[93vh] select-none bg-gray-900" data-vaul-drawer-wrapper id="page"> <div class="relative flex flex-col flex-grow h-[93vh]" data-vaul-drawer-wrapper id="page">
<div class="relative overflow-auto h-full shadow-md rounded-lg"> <div class="relative overflow-auto h-full shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-400"> <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-800 text-gray-200 sticky top-0"> <thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
<tr> <tr>
<th class="px-6 py-4 font-semibold w-2/12" scope="col">Date</th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-800 w-2/12" scope="col"> Date </th>
<th class="px-6 py-4 font-semibold" scope="col">Weight</th> <th class="px-6 py-3 w-10/12" scope="col"> Weight </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-700"> <tbody>
<EmptyWeightComp /> <EmptyWeightComp />
{#each items as f} {#each items as f}
<WeightComp item={f} dateColor={getDateColor(f)} /> <WeightComp item={f} dateColor={getDateColor(f)} />
@@ -38,5 +40,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div></div>
</div> </div>
</template> </template>

View File

@@ -1,29 +1,28 @@
<script lang="ts"> <script lang="ts">
import Router from "svelte-spa-router"; import Router from 'svelte-spa-router'
import Energy from "./routes/Energy/Energy.svelte"; import Energy from './routes/Energy/Energy.svelte'
import Daily from "./routes/Energy/Daily.svelte"; import Daily from './routes/Energy/Daily.svelte'
import Weekly from "./routes/Energy/Weekly.svelte"; import Weekly from './routes/Energy/Weekly.svelte'
import Monthly from "./routes/Energy/Monthly.svelte"; import Monthly from './routes/Energy/Monthly.svelte'
import Yearly from "./routes/Energy/Yearly.svelte"; import Yearly from './routes/Energy/Yearly.svelte'
import Weight from "./routes/Weight/Weight.svelte"; import Weight from './routes/Weight/Weight.svelte'
import WDaily from "./routes/Weight/WDaily.svelte"; import WDaily from './routes/Weight/WDaily.svelte'
import WWeekly from "./routes/Weight/WWeekly.svelte"; import WWeekly from './routes/Weight/WWeekly.svelte'
import WMonthly from "./routes/Weight/WMonthly.svelte"; import WMonthly from './routes/Weight/WMonthly.svelte'
import WYearly from "./routes/Weight/WYearly.svelte"; import WYearly from './routes/Weight/WYearly.svelte'
const routes = { const routes = {
"/": Energy, '/': Energy,
"/Energy": Energy, '/Energy/daily': Daily,
"/Energy/daily": Daily, '/Energy/weekly': Weekly,
"/Energy/weekly": Weekly, '/Energy/monthly': Monthly,
"/Energy/monthly": Monthly, '/Energy/yearly': Yearly,
"/Energy/yearly": Yearly, '/Weight': Weight,
"/Weight": Weight, '/Weight/daily': WDaily,
"/Weight/daily": WDaily, '/Weight/weekly': WWeekly,
"/Weight/weekly": WWeekly, '/Weight/monthly': WMonthly,
"/Weight/monthly": WMonthly, '/Weight/yearly': WYearly
"/Weight/yearly": WYearly, }
};
</script> </script>
<Router {routes} /> <Router {routes} />

View File

@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import FoodTable from "$lib/components/Energy/FoodTable.svelte"; import FoodTable from "$lib/components/Energy/FoodTable.svelte";
import * as srouter from "svelte-spa-router";
import { location } from "svelte-spa-router";
import { foodStore } from "$lib/store/Energy/foodStore"; import { foodStore } from "$lib/store/Energy/foodStore";
// Fuck this hacky shit // Fuck this hacky shit
@@ -11,16 +9,16 @@
// Not when pushing unshifting the store // Not when pushing unshifting the store
// This is the only thing that works // This is the only thing that works
// Hacky ass shit // Hacky ass shit
let forceUpdate = false; let forceUpdate = false;
foodStore.subscribe(() => { foodStore.subscribe(() => {
forceUpdate = !forceUpdate; forceUpdate = !forceUpdate;
}); });
</script> </script>
<template> <template>
{#if forceUpdate} {#if forceUpdate}
<FoodTable items={$foodStore} /> <FoodTable items={$foodStore} />
{:else} {:else}
<FoodTable items={$foodStore} /> <FoodTable items={$foodStore} />
{/if} {/if}
</template> </template>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import AggregatedWeightTable from "$components/Weight/Aggregated/AggregatedWeightTable.svelte"; import AggregatedWeightTable from '$components/Weight/Aggregated/AggregatedWeightTable.svelte'
import { dailyWeightStore } from "$lib/store/Weight/dailyWeightStore"; import { dailyWeightStore } from '$lib/store/Weight/dailyWeightStore'
</script> </script>
<template> <template>
<AggregatedWeightTable items={$dailyWeightStore} /> <AggregatedWeightTable items="{$dailyWeightStore}" />
</template> </template>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { monthlyWeightStore } from "$lib/store/Weight/monthlyWeightStore"; import { monthlyWeightStore } from '$lib/store/Weight/monthlyWeightStore'
import AggregatedWeightTable from "$components/Weight/Aggregated/AggregatedWeightTable.svelte"; import AggregatedWeightTable from '$components/Weight/Aggregated/AggregatedWeightTable.svelte'
</script> </script>
<template> <template>
<AggregatedWeightTable items={$monthlyWeightStore} /> <AggregatedWeightTable items="{$monthlyWeightStore}" />
</template> </template>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { weeklyWeightStore } from "$lib/store/Weight/weeklyWeightStore"; import { weeklyWeightStore } from '$lib/store/Weight/weeklyWeightStore'
import AggregatedWeightTable from "$components/Weight/Aggregated/AggregatedWeightTable.svelte"; import AggregatedWeightTable from '$components/Weight/Aggregated/AggregatedWeightTable.svelte'
</script> </script>
<template> <template>
<AggregatedWeightTable items={$weeklyWeightStore} /> <AggregatedWeightTable items="{$weeklyWeightStore}" />
</template> </template>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import AggregatedWeightTable from "$components/Weight/Aggregated/AggregatedWeightTable.svelte"; import AggregatedWeightTable from '$components/Weight/Aggregated/AggregatedWeightTable.svelte'
import { yearlyWeightStore } from "$lib/store/Weight/yearlyWeightStore"; import { yearlyWeightStore } from '$lib/store/Weight/yearlyWeightStore'
</script> </script>
<template> <template>
<AggregatedWeightTable items={$yearlyWeightStore} /> <AggregatedWeightTable items="{$yearlyWeightStore}" />
</template> </template>

View File

@@ -4,6 +4,7 @@
let forceUpdate = false; let forceUpdate = false;
weightStore.subscribe(() => { weightStore.subscribe(() => {
console.log("updte");
forceUpdate = !forceUpdate; forceUpdate = !forceUpdate;
}); });
</script> </script>

View File

@@ -1,6 +1,5 @@
import { cubicOut } from "svelte/easing"; import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition"; import type { TransitionConfig } from "svelte/transition";
import regression from "regression";
type FlyAndScaleParams = { type FlyAndScaleParams = {
y?: number; y?: number;
@@ -69,11 +68,8 @@ function GenerateRandomHSL(): Color {
const existingColors: Color[] = []; const existingColors: Color[] = [];
function RemoveExistingColors() {
existingColors.length = 0;
}
function GenerateColor(): string { function GenerateColor(): string {
const minDistance = 5; const minDistance = 15;
let newColor: Color; let newColor: Color;
let isDistinct = false; let isDistinct = false;
@@ -93,9 +89,7 @@ function GenerateColor(): string {
break; break;
} }
} }
if (isDistinct) { existingColors.push(newColor);
existingColors.push(newColor);
}
} }
// We can not reach this point without having a color generated // We can not reach this point without having a color generated
@@ -110,10 +104,5 @@ function LerpColor(color1: Color, color2: Color, t: number): Color {
return { h, s, l }; return { h, s, l };
} }
function CalculateR2(input: [number, number][]): number[] { export { GenerateColor, LerpColor };
const reg = regression.linear(input);
return reg.points.map((point: [number, number]) => point[1]);
}
export { GenerateColor, LerpColor, RemoveExistingColors, CalculateR2 };
export type { Color }; export type { Color };

View File

@@ -1,8 +1,8 @@
import "./style.css"; import './style.css'
import App from "./App.svelte"; import App from './App.svelte'
const app = new App({ const app = new App({
target: document.getElementById("app"), target: document.getElementById('app')
}); })
export default app; export default app

View File

@@ -3,26 +3,28 @@
@tailwind utilities; @tailwind utilities;
html { html {
background-color: rgba(27, 38, 54, 1); background-color: rgba(27, 38, 54, 1);
text-align: center; text-align: center;
color: white; color: white;
} }
body { body {
margin: 0; margin: 0;
color: white; color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
} }
@font-face { @font-face {
font-family: "Nunito"; font-family: "Nunito";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local(""), url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
} }
#app { #app {
height: 100vh; height: 100vh;
text-align: center; text-align: center;
} }

View File

@@ -2,8 +2,6 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {main} from '../models'; import {main} from '../models';
export function Close():Promise<void>;
export function CreateFood(arg1:main.Food):Promise<main.WailsFood1>; export function CreateFood(arg1:main.Food):Promise<main.WailsFood1>;
export function CreateWeight(arg1:main.Weight):Promise<main.WailsWeight1>; export function CreateWeight(arg1:main.Weight):Promise<main.WailsWeight1>;
@@ -14,7 +12,7 @@ export function GetDailyWeight():Promise<main.WailsAggregateWeight>;
export function GetFood():Promise<main.WailsFood>; export function GetFood():Promise<main.WailsFood>;
export function GetLastPer100(arg1:string):Promise<main.WailsFoodSearch>; export function GetLastPer100(arg1:string):Promise<main.WailsPer100>;
export function GetMonthlyFood():Promise<main.WailsAggregateFood>; export function GetMonthlyFood():Promise<main.WailsAggregateFood>;

View File

@@ -2,10 +2,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export function Close() {
return window['go']['main']['App']['Close']();
}
export function CreateFood(arg1) { export function CreateFood(arg1) {
return window['go']['main']['App']['CreateFood'](arg1); return window['go']['main']['App']['CreateFood'](arg1);
} }

View File

@@ -33,7 +33,7 @@ export namespace main {
} }
} }
export class Food { export class Food {
id: number; rowid: number;
date: string; date: string;
food: string; food: string;
description: string; description: string;
@@ -47,7 +47,7 @@ export namespace main {
constructor(source: any = {}) { constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"]; this.rowid = source["rowid"];
this.date = source["date"]; this.date = source["date"];
this.food = source["food"]; this.food = source["food"];
this.description = source["description"]; this.description = source["description"];
@@ -56,32 +56,6 @@ export namespace main {
this.energy = source["energy"]; this.energy = source["energy"];
} }
} }
export class FoodSearch {
id: number;
date: string;
food: string;
description: string;
amount: number;
per100: number;
energy: number;
score: number;
static createFrom(source: any = {}) {
return new FoodSearch(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.date = source["date"];
this.food = source["food"];
this.description = source["description"];
this.amount = source["amount"];
this.per100 = source["per100"];
this.energy = source["energy"];
this.score = source["score"];
}
}
export class WailsAggregateFood { export class WailsAggregateFood {
data: AggregatedFood[]; data: AggregatedFood[];
success: boolean; success: boolean;
@@ -218,40 +192,6 @@ export namespace main {
return a; return a;
} }
} }
export class WailsFoodSearch {
data: FoodSearch[];
success: boolean;
error?: string;
static createFrom(source: any = {}) {
return new WailsFoodSearch(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], FoodSearch);
this.success = source["success"];
this.error = source["error"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class WailsGenericAck { export class WailsGenericAck {
success: boolean; success: boolean;
error?: string; error?: string;
@@ -266,8 +206,24 @@ export namespace main {
this.error = source["error"]; this.error = source["error"];
} }
} }
export class WailsPer100 {
data: number;
success: boolean;
error?: string;
static createFrom(source: any = {}) {
return new WailsPer100(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = source["data"];
this.success = source["success"];
this.error = source["error"];
}
}
export class Weight { export class Weight {
id: number; rowid: number;
date: string; date: string;
weight: number; weight: number;
@@ -277,7 +233,7 @@ export namespace main {
constructor(source: any = {}) { constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"]; this.rowid = source["rowid"];
this.date = source["date"]; this.date = source["date"];
this.weight = source["weight"]; this.weight = source["weight"];
} }
@@ -366,7 +322,6 @@ export namespace main {
weightYearlyLookback: number; weightYearlyLookback: number;
target: number; target: number;
limit: number; limit: number;
searchLimit: number;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new settings(source); return new settings(source);
@@ -388,7 +343,6 @@ export namespace main {
this.weightYearlyLookback = source["weightYearlyLookback"]; this.weightYearlyLookback = source["weightYearlyLookback"];
this.target = source["target"]; this.target = source["target"];
this.limit = source["limit"]; this.limit = source["limit"];
this.searchLimit = source["searchLimit"];
} }
} }

38
main.go
View File

@@ -24,7 +24,7 @@ func init() {
logFile, err := os.Create("main.log") logFile, err := os.Create("main.log")
if err != nil { if err != nil {
log.Printf("Error creating log file: %v", err) log.Printf("Error creating log file: %v", err)
return os.Exit(1)
} }
logger := io.MultiWriter(os.Stdout, logFile) logger := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(logger) log.SetOutput(logger)
@@ -42,59 +42,33 @@ var (
//go:embed food.ddl //go:embed food.ddl
var foodDDL string var foodDDL string
//go:embed spellfix.dll
var spellfixDll []byte
// TODO: Embed food.ddl and create DB if no exists // TODO: Embed food.ddl and create DB if no exists
// TODO: Add averages to graphs (ie. R2) https://stackoverflow.com/questions/60622195/how-to-draw-a-linear-regression-line-in-chart-js // TODO: Add averages to graphs (ie. R2) https://stackoverflow.com/questions/60622195/how-to-draw-a-linear-regression-line-in-chart-js
func main() { func main() {
_, err := os.Open("spellfix.dll")
if err != nil && !os.IsNotExist(err) {
Error.Printf("Error looking for spellfix.dll: %v", err)
return
}
if err != nil && os.IsNotExist(err) {
log.Printf("No spellfix.dll found, creating...")
file, err := os.Create("spellfix.dll")
if err != nil {
Error.Printf("Error creating spellfix.dll: %v", err)
return
}
_, err = file.Write(spellfixDll)
if err != nil {
Error.Printf("Error writing spellfix.dll: %v", err)
return
}
file.Close()
}
dbpath := flag.String("db", "food.db", "Path to the database file") dbpath := flag.String("db", "food.db", "Path to the database file")
flag.Parse() flag.Parse()
db := DB{path: *dbpath} db := DB{path: *dbpath}
err = db.Open() err := db.Open()
if err != nil { if err != nil {
Error.Printf("%++v", err) Error.Printf("%++v", err)
return os.Exit(1)
} }
defer db.Close() defer db.Close()
err = db.Init(foodDDL) db.Init(foodDDL)
if err != nil {
Error.Printf("Error initializing database: %++v", err)
return
}
settingsService = &SettingsService{db: &db} settingsService = &SettingsService{db: &db}
err = settingsService.LoadSettings() err = settingsService.LoadSettings()
if err != nil { if err != nil {
Error.Printf("%++v", err) Error.Printf("%++v", err)
return os.Exit(1)
} }
log.Printf("Loaded settings as: %++v", Settings) log.Printf("Loaded settings as: %++v", Settings)
foodService = &FoodService{db: &db} foodService = &FoodService{db: &db}
weightService = &WeightService{db: &db} weightService = &WeightService{db: &db}
// Create an instance of the app structure // Create an instance of the app structure
app := NewApp() app := NewApp()

View File

@@ -28,9 +28,8 @@ type settings struct {
WeightMonthlyLookback int `default:"4" json:"weightMonthlyLookback"` WeightMonthlyLookback int `default:"4" json:"weightMonthlyLookback"`
WeightYearlyLookback int `default:"2" json:"weightYearlyLookback"` WeightYearlyLookback int `default:"2" json:"weightYearlyLookback"`
Target int `default:"2000" json:"target"` Target int `default:"2000" json:"target"`
Limit int `default:"2500" json:"limit"` Limit int `default:"2500" json:"limit"`
SearchLimit int `default:"5" json:"searchLimit"`
} }
var Settings settings var Settings settings

Binary file not shown.

View File

@@ -28,11 +28,6 @@ type (
Success bool `json:"success"` Success bool `json:"success"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
WailsFoodSearch struct {
Data []FoodSearch `json:"data"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
WailsWeight struct { WailsWeight struct {
Data []Weight `json:"data"` Data []Weight `json:"data"`

View File

@@ -10,7 +10,7 @@ type (
db *DB db *DB
} }
Weight struct { Weight struct {
Id int64 `json:"id"` Rowid int64 `json:"rowid"`
Date string `json:"date"` Date string `json:"date"`
Weight float32 `json:"weight"` Weight float32 `json:"weight"`
} }
@@ -20,11 +20,11 @@ type (
} }
) )
const weightcolumns = "id, date, weight" const weightcolumns = "rowid, date, weight"
const weightAggregatedColumns = "period, amount" const weightAggregatedColumns = "period, amount"
func (w *WeightService) GetRecent() ([]Weight, error) { func (w *WeightService) GetRecent() ([]Weight, error) {
res := []Weight{} var res []Weight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get recent weight, db is nil or is not ready") return res, fmt.Errorf("cannot get recent weight, db is nil or is not ready")
} }
@@ -37,7 +37,7 @@ func (w *WeightService) GetRecent() ([]Weight, error) {
for row.Next() { for row.Next() {
var weight Weight var weight Weight
err := row.Scan(&weight.Id, &weight.Date, &weight.Weight) err := row.Scan(&weight.Rowid, &weight.Date, &weight.Weight)
if err != nil { if err != nil {
log.Printf("error scanning row: %v", err) log.Printf("error scanning row: %v", err)
continue continue
@@ -62,22 +62,22 @@ func (w *WeightService) Create(weight Weight) (Weight, error) {
return weight, fmt.Errorf("error inserting weight: %v", err) return weight, fmt.Errorf("error inserting weight: %v", err)
} }
id, err := res.LastInsertId() rowid, err := res.LastInsertId()
if err != nil { if err != nil {
return weight, fmt.Errorf("error getting last insert id: %v", err) return weight, fmt.Errorf("error getting last insert id: %v", err)
} }
return w.GetById(id) return w.GetByRowid(rowid)
} }
func (w *WeightService) GetById(id int64) (Weight, error) { func (w *WeightService) GetByRowid(rowid int64) (Weight, error) {
var res Weight var res Weight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get weight by id, db is nil or is not ready") return res, fmt.Errorf("cannot get weight by rowid, db is nil or is not ready")
} }
row := w.db.readConn.QueryRow(fmt.Sprintf("SELECT %s from weightView WHERE id = ?", weightcolumns), id) row := w.db.readConn.QueryRow(fmt.Sprintf("SELECT %s from weightView WHERE rowid = ?", weightcolumns), rowid)
err := row.Scan(&res.Id, &res.Date, &res.Weight) err := row.Scan(&res.Rowid, &res.Date, &res.Weight)
if err != nil { if err != nil {
return res, fmt.Errorf("error scanning row: %v", err) return res, fmt.Errorf("error scanning row: %v", err)
} }
@@ -88,7 +88,7 @@ func (w *WeightService) GetById(id int64) (Weight, error) {
// I could probably refactor this to be less of a disaster... // I could probably refactor this to be less of a disaster...
// But I think it'll work for now // But I think it'll work for now
func (w *WeightService) GetDaily() ([]AggregatedWeight, error) { func (w *WeightService) GetDaily() ([]AggregatedWeight, error) {
res := []AggregatedWeight{} var res []AggregatedWeight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get daily weight, db is nil or is not ready") return res, fmt.Errorf("cannot get daily weight, db is nil or is not ready")
} }
@@ -114,7 +114,7 @@ func (w *WeightService) GetDaily() ([]AggregatedWeight, error) {
} }
func (w *WeightService) GetWeekly() ([]AggregatedWeight, error) { func (w *WeightService) GetWeekly() ([]AggregatedWeight, error) {
res := []AggregatedWeight{} var res []AggregatedWeight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get weekly weight, db is nil or is not ready") return res, fmt.Errorf("cannot get weekly weight, db is nil or is not ready")
} }
@@ -140,7 +140,7 @@ func (w *WeightService) GetWeekly() ([]AggregatedWeight, error) {
} }
func (w *WeightService) GetMonthly() ([]AggregatedWeight, error) { func (w *WeightService) GetMonthly() ([]AggregatedWeight, error) {
res := []AggregatedWeight{} var res []AggregatedWeight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get monthly weight, db is nil or is not ready") return res, fmt.Errorf("cannot get monthly weight, db is nil or is not ready")
} }
@@ -166,7 +166,7 @@ func (w *WeightService) GetMonthly() ([]AggregatedWeight, error) {
} }
func (w *WeightService) GetYearly() ([]AggregatedWeight, error) { func (w *WeightService) GetYearly() ([]AggregatedWeight, error) {
res := []AggregatedWeight{} var res []AggregatedWeight
if w.db == nil || !w.db.Ready { if w.db == nil || !w.db.Ready {
return res, fmt.Errorf("cannot get yearly weight, db is nil or is not ready") return res, fmt.Errorf("cannot get yearly weight, db is nil or is not ready")
} }