Compare commits
483 Commits
e507fbc292
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 106d3b1fa1 | |||
| b9f6b77286 | |||
| 3c8fd843e6 | |||
| e61af3007f | |||
| 3594baceb5 | |||
| d534779fad | |||
| a776c88cab | |||
| 72de7c7648 | |||
| a8b150857c | |||
| dd4f508346 | |||
| f21ee57632 | |||
| 0e42c9002b | |||
| 9e60a41f0a | |||
| eaff553dc9 | |||
| 6880811236 | |||
| 38bda46dcf | |||
| bd86ad499b | |||
| 838ab37fc1 | |||
| 9948d2521b | |||
| 64abf79f9c | |||
| 0d41a65435 | |||
| ecd1529130 | |||
| 015a7cb5cd | |||
| 980b42aa44 | |||
| 649cfe0b02 | |||
| 1fb9616aa7 | |||
| 013447fa90 | |||
| 221afb599b | |||
| f8619d3ef7 | |||
| f6393c9a59 | |||
| 561064a194 | |||
| 3015d7bac2 | |||
| a3345afbfa | |||
| f078ac7d0b | |||
| e28d9e5d16 | |||
| 29c56bee1c | |||
| 3ebc0810e7 | |||
| 0c595f76a3 | |||
| 176d2b0bd4 | |||
| 115d08a245 | |||
| b4b600bd7c | |||
| ce2cd977ac | |||
| 8b6b9453a8 | |||
| 2dd9f33303 | |||
| 94ee8bdb7e | |||
| 5d1c758451 | |||
| 00359e2e8d | |||
| 95330c163b | |||
| 84a0996be9 | |||
| 48579267b5 | |||
| 8b54d502f2 | |||
| e45688d57e | |||
| f7c9c97f0a | |||
| 76924a0332 | |||
| d97593d487 | |||
| de96f12b55 | |||
| 70161da3ed | |||
| 3a182fc49b | |||
| ec7bd469f9 | |||
| 6523b10699 | |||
| 61d2b81e8c | |||
| fe0968716d | |||
| 769f3981cd | |||
| a78f766122 | |||
| 10cea769bf | |||
| f5e65524aa | |||
| 390a216260 | |||
| 3e57d10360 | |||
| 28a4b37dde | |||
| 4de4431390 | |||
| 5ff7788a7b | |||
| 13170a33e8 | |||
| 5024933852 | |||
| 706d562e3e | |||
| fda09ae07a | |||
| 5de5e0b56e | |||
| a0bf27dd16 | |||
| 3d05ff708e | |||
| ee109f05a0 | |||
| f4d8c9f083 | |||
| a1af3feb1d | |||
| 8597584cf0 | |||
| 88d033314e | |||
| 9cae780431 | |||
| fa71f68de5 | |||
| 0058cdce40 | |||
| 37f966e508 | |||
| 59bf884f5d | |||
| 2ac996db73 | |||
| e19e6562bb | |||
| a283bc1bcd | |||
| 1b816e512a | |||
| 4d0dcccf94 | |||
| bc4e4ab36c | |||
| a663f27fb2 | |||
| 744b300d00 | |||
| 818a163235 | |||
| 2b1eb2b948 | |||
| 018f0e96d4 | |||
| d5594c6e32 | |||
| 7d9845737e | |||
| 68010503ab | |||
| 251e2bc553 | |||
| 5d0fa51e01 | |||
| ad4967a97d | |||
| eb0914c9ca | |||
| 5c8e0094f4 | |||
| 4b85cae22c | |||
| 75b2cc53a4 | |||
| bb3ae507ea | |||
| 0d5e6146f2 | |||
| ec18cb0ee0 | |||
| 510cb3012b | |||
| 6e2c6acd9d | |||
| 5130691ab9 | |||
| 1a9731c4bb | |||
| 300a4925df | |||
| 4870a8b1b1 | |||
| 275ae4598f | |||
| eeb6d2bb3b | |||
| a89c6dc658 | |||
| c6ad67345e | |||
| 459c8e1c4e | |||
| ec4e8b7e2a | |||
| 5a1f3bb75b | |||
| d4b14605c1 | |||
| 00e530df4b | |||
| 16b43ec561 | |||
| 6d235eea36 | |||
| 27ad03b1c1 | |||
| 1a845c7846 | |||
| 3c3a25bcfc | |||
| e508f03abb | |||
| e74975a52a | |||
| a94c7255c6 | |||
| a65ef5f548 | |||
| be302b77d4 | |||
| a4a8c191b6 | |||
| 6482a76a51 | |||
| c62378c20a | |||
| 5cf0b66688 | |||
| 357927e2a0 | |||
| c632487d7e | |||
| cb4a03015a | |||
| 7f8b345e77 | |||
| df9c42136e | |||
| 019c4c6b0c | |||
| ad2a70aaf3 | |||
| 076e230a01 | |||
| 636bd9df0e | |||
| 3eab20049e | |||
| 64879ac9d6 | |||
| e674043daa | |||
| 71049a7f26 | |||
| e8a51ecc52 | |||
| a6a6026a11 | |||
| a9749f062e | |||
| 5a2b990c0c | |||
| b97eae10a3 | |||
| cc07ef983f | |||
| b4a0383be7 | |||
| a4b94fc6c2 | |||
| 71dfe5647e | |||
| 90b863b6cf | |||
| 64439d9041 | |||
| 2f3d643278 | |||
| 8e6424aa63 | |||
| c69ca7da5c | |||
| 7b0c84e88e | |||
| ff7960e2dd | |||
| d08fd452f9 | |||
| 0d64e90bbf | |||
| a7119dfda4 | |||
| c0f6af7a05 | |||
| ac92f80dc6 | |||
| 52cb50b168 | |||
| eaa029cce1 | |||
| 9a7654ae2d | |||
| a9ab92b7b3 | |||
| 378900d1b1 | |||
| 4fa8bfb7bd | |||
| ac4fd30b0a | |||
| 7d1498c3eb | |||
| ce32291437 | |||
| 33b8d51f89 | |||
| a2ba328097 | |||
| 23d91890f5 | |||
| 07b83aa728 | |||
| 9c325c7799 | |||
| 7970e8670c | |||
| 2deba39907 | |||
| 0a766e1ebb | |||
| 6119938e52 | |||
| a8d12b5d53 | |||
| ce8d546447 | |||
| b57a703812 | |||
| 6952aa16da | |||
| 63e3081a69 | |||
| b046a928b0 | |||
| 9860dd2dc5 | |||
| 94920c01fb | |||
| bb280f52fe | |||
| 92e346578a | |||
| e9617f86ec | |||
| 875d1d778c | |||
| 372a891f97 | |||
| 9ea466610b | |||
| 9fb926db03 | |||
| 50b8645897 | |||
| 4541b366e5 | |||
| 4ed42678f1 | |||
| a93fd7500a | |||
| b50ca077e0 | |||
| 12cd338967 | |||
| 2a838c81f2 | |||
| cd39559834 | |||
| 4c5f3d92e6 | |||
| 54bb75956c | |||
| 0a2d27c150 | |||
| a05a625516 | |||
| 7e9b33f625 | |||
| 9f3a2a473a | |||
| 61e9258538 | |||
| c8d9ae7aff | |||
| 2eda77827a | |||
| afd2e03234 | |||
| 3a3acc4a1c | |||
| d102ab3f6e | |||
| 9b006836c6 | |||
| 385a0cd186 | |||
| 365ef387dd | |||
| 4922df6682 | |||
| a9ecd5818a | |||
| 84d66a1c3b | |||
| d34805030f | |||
| 78a28dee8d | |||
| 7f7a2975af | |||
| 151142fa9b | |||
| fa187b3a79 | |||
| b27e191e5c | |||
| e2a4b85d15 | |||
| f1500837e0 | |||
| e6c027aca7 | |||
| 495cd742b0 | |||
| 8cdb4367c7 | |||
| 1388383909 | |||
| 3cd60d4dfc | |||
| 526044d1e3 | |||
| 90ea845521 | |||
| dcd3bbb4fb | |||
| 7aef91c5e0 | |||
| 9245187056 | |||
| e84655a181 | |||
| 9a25d2e839 | |||
| f02b22f2fa | |||
| 6e9dc81e2b | |||
| 08b4175b73 | |||
| fa5d38d796 | |||
| fdb607caea | |||
| 169b95c450 | |||
| 191ed3db40 | |||
| 88bb2fafe2 | |||
| a859abfc17 | |||
| 8cad29a661 | |||
| a5d74a97a6 | |||
| cf71d26f14 | |||
| 7e31af27f1 | |||
| 78fe25497b | |||
| dc83bdb3fb | |||
| f6f31540af | |||
| 2eb346bb6a | |||
| 2b022c31cb | |||
| c3f4403145 | |||
| 1d07fa271d | |||
| 839a1af51b | |||
| 0324216753 | |||
| 335d4403f1 | |||
| 89ba950c5b | |||
| 2b8e0695c6 | |||
| d448a41a9f | |||
| a69d4e4d55 | |||
| 6edc1e2915 | |||
| 57f1e70c98 | |||
| 1b1f957e01 | |||
| 49969b0608 | |||
| 9b95ffb59e | |||
| c9560f6881 | |||
| c5535a5b3b | |||
| 5ab0d13b21 | |||
| 15289e4965 | |||
| 181da1f09d | |||
| 90b90a8185 | |||
| fb30eb4ad6 | |||
| 5454a1cfaf | |||
| 3716d22eca | |||
| 6d2f0c6108 | |||
| 61c158d5b6 | |||
| 82331c0833 | |||
| e42aa75639 | |||
| fa486153b4 | |||
| aacecfffac | |||
| e89a342751 | |||
| e16b6f4529 | |||
| 6ddae3426d | |||
| 67468bddb6 | |||
| 10bc0a04a2 | |||
| 8a57236f04 | |||
| b138661991 | |||
| 6db9bb2ab3 | |||
| 6ae2458186 | |||
| 51d36bf15b | |||
| ecc2da5f86 | |||
| d7ab3f56dc | |||
| 55aa1e67ba | |||
| 1f83b721a6 | |||
| 0596ea2b1e | |||
| 3c1f6ba40f | |||
| 0eff145f02 | |||
| 1fa1db7d1b | |||
| a1369719d7 | |||
| 40ddf737c8 | |||
| ad14254ecb | |||
| e8d996cec5 | |||
| 0ed6b4c123 | |||
| 0bc556f47c | |||
| 5a530b2e39 | |||
| 868c8e6409 | |||
| 30143019d6 | |||
| cd5dd347d3 | |||
| ab09378fcd | |||
| 18f85a8929 | |||
| 55614b34c7 | |||
| 664918f431 | |||
| 048fc38032 | |||
| 2f26b5dfd9 | |||
| 4f6c198307 | |||
| c99d6e4e6b | |||
| b97cf63484 | |||
| 7af536bd9c | |||
| 5406e79fc8 | |||
| 0e88f77474 | |||
| 878a47ffd1 | |||
| eba4268718 | |||
| 5ae6a3403f | |||
| 3156cea904 | |||
| d432d16752 | |||
| 98328be39d | |||
| 4d903f40bf | |||
| 24bed2aafb | |||
| 349dcc2275 | |||
| 47c871523d | |||
| dcfed6a746 | |||
| 91b9e5402e | |||
| cf7d5e0305 | |||
| 9bb07c1b9b | |||
| 959b741fcb | |||
| 91cc54aaec | |||
| d786ab15c9 | |||
| 47e65e1609 | |||
| 91dd2f54ef | |||
| 42771ea958 | |||
| 77a0901352 | |||
| a43efa014f | |||
| 4990cf9c43 | |||
| 9660c99a14 | |||
| f89de6db50 | |||
| 6290c4b843 | |||
| fba1618888 | |||
| 5fee1f9ccc | |||
| 3960203d26 | |||
| 2302ba5eeb | |||
| c9a6c83649 | |||
| 3294c1854c | |||
| 29a5adb40a | |||
| 51dc8daf35 | |||
| a22c56fd2c | |||
| 11c5c8921b | |||
| 1a503c8320 | |||
| f169fd2ba2 | |||
| d36dec8d60 | |||
| e065492dd4 | |||
| 26c6edb6ba | |||
| 5c5df168ad | |||
| e101070851 | |||
| 5278727c51 | |||
| 9a354c38a5 | |||
| cd8375ce0f | |||
| 6549643340 | |||
| 33fb206e2f | |||
| 49f1990341 | |||
| 40392e6da3 | |||
| d3bc840555 | |||
| ede5f16dc1 | |||
| 75132503c0 | |||
| 393eaea2f4 | |||
| a385ef21cf | |||
| a37818fc49 | |||
| 0d3f86532e | |||
| 55e50d31ca | |||
| 1b2a99a3c8 | |||
| ae62d2bea5 | |||
| 0126125837 | |||
| c5278554cc | |||
| bb5f2bc2fe | |||
| b7ed4e2169 | |||
| c609b45d99 | |||
| c817654f3e | |||
| 3f53317c06 | |||
| 254edf3421 | |||
| 0814e19a68 | |||
| 382a1f53bd | |||
| f90876f499 | |||
| caf168c7a1 | |||
| 4c85f1de79 | |||
| 410df01b4d | |||
| 13e5ed9f9e | |||
| dfb4b34de3 | |||
| 7b6c7090f8 | |||
| 87869543f7 | |||
| 1cd4698969 | |||
| 4ea817e81f | |||
| 3541a4755c | |||
| ea5802b61b | |||
| cf703f3eee | |||
| 84881c5c2d | |||
| 992a8ea282 | |||
| f7382b0d2b | |||
| 47dd025ae3 | |||
| f114ca06d8 | |||
| 20213ff17b | |||
| 9932568986 | |||
| 3a0f93e406 | |||
| b09063f74a | |||
| b3b37d252d | |||
| 3c71fddbd2 | |||
| a3e1db3d77 | |||
| 5a766b8371 | |||
| fd804ae515 | |||
| 4b120982d0 | |||
| 7582e4d8d9 | |||
| 8acf25a2a7 | |||
| e505a1617e | |||
| 028e45bb7a | |||
| 536a49fe1c | |||
| 40e854fb87 | |||
| 5df6c67ee5 | |||
| 05263d1089 | |||
| 1bc1b79042 | |||
| 863716c096 | |||
| 53ebbb6e8d | |||
| bf07c18fd7 | |||
| d212584486 | |||
| 1424ec22f4 | |||
| e595783d89 | |||
| 2df18869e5 | |||
| ee69d9c2fe | |||
| 7e7f3ff732 | |||
| 3fe48464e4 | |||
| d1d6ee6762 | |||
| ad61b8e1fa | |||
| d8095b0c67 | |||
| 410270e217 | |||
| 5bec6c9590 | |||
| 971f705288 | |||
| 13ebd80ce9 | |||
| b99432c202 | |||
| ee0587a16b | |||
| 64f6bde6a9 | |||
| f49589907a | |||
| 2115da85b5 | |||
| 43092fa4f5 | |||
| 46e4043994 | |||
| 24ef31e00f | |||
| 993fbb30eb | |||
| 8cc7e4002f | |||
| 1fc1079484 | |||
| df16298b1f | |||
| f4690b52a9 | |||
| 81590fe622 | |||
| 97b1619b01 | |||
| c0ce4892cd | |||
| 90431f824a | |||
| fe60149769 |
38
.cursor/rules/frontend-rules.mdc
Normal file
38
.cursor/rules/frontend-rules.mdc
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
You are an expert AI programming assistant focused on producing clean, readable TypeScript and Rust code for modern cross-platform desktop apps.
|
||||
|
||||
Use these rules for any code under /frontend folder.
|
||||
|
||||
You always use the latest versions of Tauri, Rust, SolidJS, and you're fluent in their latest features, best practices, and patterns.
|
||||
|
||||
You give accurate, thoughtful answers and think like a real dev—step-by-step.
|
||||
|
||||
Follow the user’s specs exactly. If a specs folder exists, check it before coding.
|
||||
|
||||
Begin with a detailed pseudo-code plan and confirm it with the user before writing actual code.
|
||||
|
||||
Write correct, complete, idiomatic, secure, performant, and bug-free code.
|
||||
|
||||
Prioritize readability unless performance is explicitly required.
|
||||
|
||||
Fully implement all requested features—no TODOs, stubs, or placeholders.
|
||||
|
||||
Use TypeScript's type system thoroughly for clarity and safety.
|
||||
|
||||
Style with TailwindCSS using utility-first principles.
|
||||
|
||||
Use Kobalte components effectively, building with Solid’s reactive model in mind.
|
||||
|
||||
Offload performance-heavy logic to Rust and ensure smooth integration with Tauri.
|
||||
|
||||
Guarantee tight coordination between SolidJS, Tauri, and Rust for a polished desktop UX.
|
||||
|
||||
When needed, provide bash scripts to generate config files or folder structures.
|
||||
|
||||
Be concise—cut the fluff.
|
||||
|
||||
If there's no solid answer, say so. If you're unsure, don't guess—own it.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
.env
|
||||
db
|
||||
screenmark
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "haystack"]
|
||||
path = haystack-arch
|
||||
url = https://aur.archlinux.org/haystack
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer", "biomejs.biome", "golang.go", "bradlc.vscode-tailwindcss"]
|
||||
}
|
||||
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"biome.enabled": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
"rust-analyzer.linkedProjects": ["./frontend/src-tauri/Cargo.toml"]
|
||||
}
|
||||
20
backend/.gen/haystack/haystack/enum/progress.go
Normal file
20
backend/.gen/haystack/haystack/enum/progress.go
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package enum
|
||||
|
||||
import "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
var Progress = &struct {
|
||||
NotStarted postgres.StringExpression
|
||||
InProgress postgres.StringExpression
|
||||
Complete postgres.StringExpression
|
||||
}{
|
||||
NotStarted: postgres.NewEnumValue("not-started"),
|
||||
InProgress: postgres.NewEnumValue("in-progress"),
|
||||
Complete: postgres.NewEnumValue("complete"),
|
||||
}
|
||||
23
backend/.gen/haystack/haystack/model/image.go
Normal file
23
backend/.gen/haystack/haystack/model/image.go
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
ImageName string
|
||||
Description string
|
||||
Status Progress
|
||||
Image []byte
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
19
backend/.gen/haystack/haystack/model/image_schema_items.go
Normal file
19
backend/.gen/haystack/haystack/model/image_schema_items.go
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageSchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Value *string
|
||||
SchemaItemID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
||||
18
backend/.gen/haystack/haystack/model/image_stacks.go
Normal file
18
backend/.gen/haystack/haystack/model/image_stacks.go
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageStacks struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
StackID uuid.UUID
|
||||
}
|
||||
53
backend/.gen/haystack/haystack/model/progress.go
Normal file
53
backend/.gen/haystack/haystack/model/progress.go
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import "errors"
|
||||
|
||||
type Progress string
|
||||
|
||||
const (
|
||||
Progress_NotStarted Progress = "not-started"
|
||||
Progress_InProgress Progress = "in-progress"
|
||||
Progress_Complete Progress = "complete"
|
||||
)
|
||||
|
||||
var ProgressAllValues = []Progress{
|
||||
Progress_NotStarted,
|
||||
Progress_InProgress,
|
||||
Progress_Complete,
|
||||
}
|
||||
|
||||
func (e *Progress) Scan(value interface{}) error {
|
||||
var enumValue string
|
||||
switch val := value.(type) {
|
||||
case string:
|
||||
enumValue = val
|
||||
case []byte:
|
||||
enumValue = string(val)
|
||||
default:
|
||||
return errors.New("jet: Invalid scan value for AllTypesEnum enum. Enum value has to be of type string or []byte")
|
||||
}
|
||||
|
||||
switch enumValue {
|
||||
case "not-started":
|
||||
*e = Progress_NotStarted
|
||||
case "in-progress":
|
||||
*e = Progress_InProgress
|
||||
case "complete":
|
||||
*e = Progress_Complete
|
||||
default:
|
||||
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Progress) String() string {
|
||||
return string(e)
|
||||
}
|
||||
20
backend/.gen/haystack/haystack/model/schema_items.go
Normal file
20
backend/.gen/haystack/haystack/model/schema_items.go
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Item string
|
||||
Value string
|
||||
Description string
|
||||
StackID uuid.UUID
|
||||
}
|
||||
22
backend/.gen/haystack/haystack/model/stacks.go
Normal file
22
backend/.gen/haystack/haystack/model/stacks.go
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Stacks struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
Status Progress
|
||||
Name string
|
||||
Description string
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
19
backend/.gen/haystack/haystack/model/users.go
Normal file
19
backend/.gen/haystack/haystack/model/users.go
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Email string
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
93
backend/.gen/haystack/haystack/table/image.go
Normal file
93
backend/.gen/haystack/haystack/table/image.go
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Image = newImageTable("haystack", "image", "")
|
||||
|
||||
type imageTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
ImageName postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
Image postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageTable struct {
|
||||
imageTable
|
||||
|
||||
EXCLUDED imageTable
|
||||
}
|
||||
|
||||
// AS creates new ImageTable with assigned alias
|
||||
func (a ImageTable) AS(alias string) *ImageTable {
|
||||
return newImageTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageTable with assigned schema name
|
||||
func (a ImageTable) FromSchema(schemaName string) *ImageTable {
|
||||
return newImageTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageTable with assigned table prefix
|
||||
func (a ImageTable) WithPrefix(prefix string) *ImageTable {
|
||||
return newImageTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageTable with assigned table suffix
|
||||
func (a ImageTable) WithSuffix(suffix string) *ImageTable {
|
||||
return newImageTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageTable(schemaName, tableName, alias string) *ImageTable {
|
||||
return &ImageTable{
|
||||
imageTable: newImageTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
ImageNameColumn = postgres.StringColumn("image_name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
ImageColumn = postgres.StringColumn("image")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
ImageName: ImageNameColumn,
|
||||
Description: DescriptionColumn,
|
||||
Status: StatusColumn,
|
||||
Image: ImageColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
84
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
84
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageSchemaItems = newImageSchemaItemsTable("haystack", "image_schema_items", "")
|
||||
|
||||
type imageSchemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
SchemaItemID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageSchemaItemsTable struct {
|
||||
imageSchemaItemsTable
|
||||
|
||||
EXCLUDED imageSchemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageSchemaItemsTable with assigned alias
|
||||
func (a ImageSchemaItemsTable) AS(alias string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageSchemaItemsTable with assigned schema name
|
||||
func (a ImageSchemaItemsTable) FromSchema(schemaName string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageSchemaItemsTable with assigned table prefix
|
||||
func (a ImageSchemaItemsTable) WithPrefix(prefix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageSchemaItemsTable with assigned table suffix
|
||||
func (a ImageSchemaItemsTable) WithSuffix(suffix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTable(schemaName, tableName, alias string) *ImageSchemaItemsTable {
|
||||
return &ImageSchemaItemsTable{
|
||||
imageSchemaItemsTable: newImageSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSchemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
SchemaItemIDColumn = postgres.StringColumn("schema_item_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageSchemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Value: ValueColumn,
|
||||
SchemaItemID: SchemaItemIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
81
backend/.gen/haystack/haystack/table/image_stacks.go
Normal file
81
backend/.gen/haystack/haystack/table/image_stacks.go
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageStacks = newImageStacksTable("haystack", "image_stacks", "")
|
||||
|
||||
type imageStacksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
StackID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageStacksTable struct {
|
||||
imageStacksTable
|
||||
|
||||
EXCLUDED imageStacksTable
|
||||
}
|
||||
|
||||
// AS creates new ImageStacksTable with assigned alias
|
||||
func (a ImageStacksTable) AS(alias string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageStacksTable with assigned schema name
|
||||
func (a ImageStacksTable) FromSchema(schemaName string) *ImageStacksTable {
|
||||
return newImageStacksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageStacksTable with assigned table prefix
|
||||
func (a ImageStacksTable) WithPrefix(prefix string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageStacksTable with assigned table suffix
|
||||
func (a ImageStacksTable) WithSuffix(suffix string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageStacksTable(schemaName, tableName, alias string) *ImageStacksTable {
|
||||
return &ImageStacksTable{
|
||||
imageStacksTable: newImageStacksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageStacksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageStacksTableImpl(schemaName, tableName, alias string) imageStacksTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
StackIDColumn = postgres.StringColumn("stack_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, StackIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, StackIDColumn}
|
||||
)
|
||||
|
||||
return imageStacksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
StackID: StackIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
87
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
87
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var SchemaItems = newSchemaItemsTable("haystack", "schema_items", "")
|
||||
|
||||
type schemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Item postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
StackID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type SchemaItemsTable struct {
|
||||
schemaItemsTable
|
||||
|
||||
EXCLUDED schemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new SchemaItemsTable with assigned alias
|
||||
func (a SchemaItemsTable) AS(alias string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new SchemaItemsTable with assigned schema name
|
||||
func (a SchemaItemsTable) FromSchema(schemaName string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new SchemaItemsTable with assigned table prefix
|
||||
func (a SchemaItemsTable) WithPrefix(prefix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new SchemaItemsTable with assigned table suffix
|
||||
func (a SchemaItemsTable) WithSuffix(suffix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newSchemaItemsTable(schemaName, tableName, alias string) *SchemaItemsTable {
|
||||
return &SchemaItemsTable{
|
||||
schemaItemsTable: newSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ItemColumn = postgres.StringColumn("item")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
StackIDColumn = postgres.StringColumn("stack_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
|
||||
)
|
||||
|
||||
return schemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Item: ItemColumn,
|
||||
Value: ValueColumn,
|
||||
Description: DescriptionColumn,
|
||||
StackID: StackIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
90
backend/.gen/haystack/haystack/table/stacks.go
Normal file
90
backend/.gen/haystack/haystack/table/stacks.go
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Stacks = newStacksTable("haystack", "stacks", "")
|
||||
|
||||
type stacksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type StacksTable struct {
|
||||
stacksTable
|
||||
|
||||
EXCLUDED stacksTable
|
||||
}
|
||||
|
||||
// AS creates new StacksTable with assigned alias
|
||||
func (a StacksTable) AS(alias string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new StacksTable with assigned schema name
|
||||
func (a StacksTable) FromSchema(schemaName string) *StacksTable {
|
||||
return newStacksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new StacksTable with assigned table prefix
|
||||
func (a StacksTable) WithPrefix(prefix string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new StacksTable with assigned table suffix
|
||||
func (a StacksTable) WithSuffix(suffix string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newStacksTable(schemaName, tableName, alias string) *StacksTable {
|
||||
return &StacksTable{
|
||||
stacksTable: newStacksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newStacksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newStacksTableImpl(schemaName, tableName, alias string) stacksTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return stacksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
Status: StatusColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
19
backend/.gen/haystack/haystack/table/table_use_schema.go
Normal file
19
backend/.gen/haystack/haystack/table/table_use_schema.go
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
|
||||
// this method only once at the beginning of the program.
|
||||
func UseSchema(schema string) {
|
||||
Image = Image.FromSchema(schema)
|
||||
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
|
||||
ImageStacks = ImageStacks.FromSchema(schema)
|
||||
SchemaItems = SchemaItems.FromSchema(schema)
|
||||
Stacks = Stacks.FromSchema(schema)
|
||||
Users = Users.FromSchema(schema)
|
||||
}
|
||||
81
backend/.gen/haystack/haystack/table/users.go
Normal file
81
backend/.gen/haystack/haystack/table/users.go
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Users = newUsersTable("haystack", "users", "")
|
||||
|
||||
type usersTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Email postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UsersTable struct {
|
||||
usersTable
|
||||
|
||||
EXCLUDED usersTable
|
||||
}
|
||||
|
||||
// AS creates new UsersTable with assigned alias
|
||||
func (a UsersTable) AS(alias string) *UsersTable {
|
||||
return newUsersTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UsersTable with assigned schema name
|
||||
func (a UsersTable) FromSchema(schemaName string) *UsersTable {
|
||||
return newUsersTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UsersTable with assigned table prefix
|
||||
func (a UsersTable) WithPrefix(prefix string) *UsersTable {
|
||||
return newUsersTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UsersTable with assigned table suffix
|
||||
func (a UsersTable) WithSuffix(suffix string) *UsersTable {
|
||||
return newUsersTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUsersTable(schemaName, tableName, alias string) *UsersTable {
|
||||
return &UsersTable{
|
||||
usersTable: newUsersTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUsersTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, EmailColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{EmailColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return usersTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Email: EmailColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
||||
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
screenmark
|
||||
.env.docker
|
||||
.env
|
||||
pgdata
|
||||
15
backend/Dockerfile
Normal file
15
backend/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM golang
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependency management
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o /app/haystack
|
||||
|
||||
EXPOSE 3040
|
||||
|
||||
CMD ["/app/haystack"]
|
||||
26
backend/README.md
Normal file
26
backend/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Running the backend
|
||||
|
||||
1. Create a `.env.docker` file, which must contain the following.
|
||||
|
||||
OPENAI_API_KEY=openai_key
|
||||
DB_CONNECTION=postgresql://postgres:password@database:5432/haystack_db?sslmode=disable
|
||||
|
||||
2. Use `docker-compose up` to spin up the containers.
|
||||
|
||||
You should be able to access the backend through port `3040`
|
||||
|
||||
# Methods
|
||||
|
||||
For now, we cheat and add a `userId` header which if os type `UUID`. Use the auto generated test one (fcc22dbb-7792-4595-be8e-d0439e13990a).
|
||||
|
||||
- `GET /image` | Returns all of the users image, including tags, links and text any image contains.
|
||||
- `GET /image/{imageId}` | Returns the actual image, use this to display images in the UI.
|
||||
- `POST /image/{imageNameWithExtension}` | Sends an image to the backend, saves it and sents it to open ai to later process.
|
||||
|
||||
# Architecture
|
||||
|
||||
1. The user posts an image, which gets saved on our database (all data, including images are saved on DB).
|
||||
2. We listen for table event creation, and we can process this image by sending it to OpenAI.
|
||||
3. After OpenAI responds, we write to the database.
|
||||
|
||||
This means that for now, we don't have a notification system to tell the user when their image is done processing. But will do in the future.
|
||||
258
backend/agents/client/chat.go
Normal file
258
backend/agents/client/chat.go
Normal file
@ -0,0 +1,258 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Chat struct {
|
||||
Messages []ChatMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type ChatMessage interface {
|
||||
IsResponse() bool
|
||||
}
|
||||
|
||||
// TODO: the role could be inferred from the type.
|
||||
// This would solve some bugs.
|
||||
|
||||
/*
|
||||
|
||||
Is there a world where this actually becomes the product?
|
||||
Where we build such a resilient system of AI calls that we
|
||||
can build some app builder, or even just an API system,
|
||||
with a fancy UI?
|
||||
|
||||
Manage all the complexity for the user?
|
||||
|
||||
*/
|
||||
|
||||
// =============================================
|
||||
// Messages from us to the AI.
|
||||
// =============================================
|
||||
|
||||
type UserRole = string
|
||||
|
||||
const (
|
||||
User UserRole = "user"
|
||||
System UserRole = "system"
|
||||
)
|
||||
|
||||
type ToolRole = string
|
||||
|
||||
const (
|
||||
Tool ToolRole = "tool"
|
||||
)
|
||||
|
||||
type ChatUserMessage struct {
|
||||
Role UserRole `json:"role"`
|
||||
|
||||
MessageContent `json:"MessageContent"`
|
||||
}
|
||||
|
||||
func (m ChatUserMessage) MarshalJSON() ([]byte, error) {
|
||||
switch t := m.MessageContent.(type) {
|
||||
case SingleMessage:
|
||||
return json.Marshal(&struct {
|
||||
Role UserRole `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
Role: User,
|
||||
Content: t.Content,
|
||||
})
|
||||
case ArrayMessage:
|
||||
return json.Marshal(&struct {
|
||||
Role UserRole `json:"role"`
|
||||
Content []MessageContentMessage `json:"content"`
|
||||
}{
|
||||
Role: User,
|
||||
Content: t.Content,
|
||||
})
|
||||
}
|
||||
|
||||
return []byte{}, errors.New("Unreachable")
|
||||
}
|
||||
|
||||
func (r ChatUserMessage) IsResponse() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ChatUserToolResponse struct {
|
||||
Role ToolRole `json:"role"`
|
||||
|
||||
// The name of the function we are responding to.
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ToolCallId string `json:"tool_call_id"`
|
||||
}
|
||||
|
||||
func (r ChatUserToolResponse) IsResponse() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ChatAiMessage struct {
|
||||
Role string `json:"role"`
|
||||
ToolCalls *[]ToolCall `json:"tool_calls,omitempty"`
|
||||
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (m ChatAiMessage) IsResponse() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Unique interface for message content.
|
||||
// =============================================
|
||||
|
||||
type MessageContent interface {
|
||||
IsSingleMessage() bool
|
||||
}
|
||||
|
||||
type SingleMessage struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (m SingleMessage) IsSingleMessage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type ArrayMessage struct {
|
||||
Content []MessageContentMessage `json:"content"`
|
||||
}
|
||||
|
||||
func (m ArrayMessage) IsSingleMessage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type MessageContentMessage interface {
|
||||
IsImageMessage() bool
|
||||
}
|
||||
|
||||
type TextMessageContent struct {
|
||||
TextType string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (m TextMessageContent) IsImageMessage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ImageMessageContent struct {
|
||||
ImageType string `json:"type"`
|
||||
ImageUrl ImageMessageUrl `json:"image_url"`
|
||||
}
|
||||
|
||||
type ImageMessageUrl struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (m ImageMessageContent) IsImageMessage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type ImageContentUrl struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Adjacent interfaces.
|
||||
// =============================================
|
||||
|
||||
type ToolCall struct {
|
||||
Index int `json:"index"`
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type,omitzero"`
|
||||
Function FunctionCall `json:"function"`
|
||||
}
|
||||
|
||||
type FunctionCall struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Chat methods
|
||||
// =============================================
|
||||
|
||||
func (chat *Chat) AddSystem(prompt string) {
|
||||
chat.Messages = append(chat.Messages, ChatUserMessage{
|
||||
Role: System,
|
||||
MessageContent: SingleMessage{
|
||||
Content: prompt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (chat *Chat) AddUser(msg string) {
|
||||
chat.Messages = append(chat.Messages, ChatUserMessage{
|
||||
Role: User,
|
||||
MessageContent: SingleMessage{
|
||||
Content: msg,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
|
||||
extension := filepath.Ext(imageName)
|
||||
if len(extension) == 0 {
|
||||
// TODO: could also validate for image types we support.
|
||||
// return errors.New("Image does not have extension")
|
||||
// Hacky! It seems apple doesnt add extension.
|
||||
// BIG TODO: take better metadata from the image.
|
||||
extension = "png"
|
||||
}
|
||||
|
||||
extension = extension[1:]
|
||||
encodedString := base64.StdEncoding.EncodeToString(image)
|
||||
|
||||
contentLength := 1
|
||||
if query != nil {
|
||||
contentLength += 1
|
||||
}
|
||||
|
||||
messageContent := ArrayMessage{
|
||||
Content: make([]MessageContentMessage, contentLength),
|
||||
}
|
||||
|
||||
index := 0
|
||||
|
||||
if query != nil {
|
||||
messageContent.Content[index] = TextMessageContent{
|
||||
TextType: "text",
|
||||
Text: *query,
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
messageContent.Content[index] = ImageMessageContent{
|
||||
ImageType: "image_url",
|
||||
ImageUrl: ImageMessageUrl{
|
||||
Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
|
||||
},
|
||||
}
|
||||
|
||||
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}
|
||||
chat.Messages = append(chat.Messages, arrayMessage)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chat *Chat) AddAiResponse(res ChatAiMessage) {
|
||||
chat.Messages = append(chat.Messages, res)
|
||||
}
|
||||
|
||||
func (chat *Chat) AddToolResponse(res ChatUserToolResponse) {
|
||||
chat.Messages = append(chat.Messages, res)
|
||||
}
|
||||
|
||||
func (chat Chat) GetLatest() (ChatMessage, error) {
|
||||
if len(chat.Messages) == 0 {
|
||||
return nil, errors.New("Not enough messages")
|
||||
}
|
||||
|
||||
return chat.Messages[len(chat.Messages)-1], nil
|
||||
}
|
||||
24
backend/agents/client/chat_test.go
Normal file
24
backend/agents/client/chat_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFlatMarshallSingleMessage(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
message := ChatUserMessage{
|
||||
Role: User,
|
||||
MessageContent: SingleMessage{
|
||||
Content: "Hello",
|
||||
},
|
||||
}
|
||||
|
||||
json, err := json.Marshal(message)
|
||||
require.NoError(err)
|
||||
|
||||
require.Equal(string(json), "{\"role\":\"user\",\"content\":\"Hello\"}")
|
||||
}
|
||||
307
backend/agents/client/client.go
Normal file
307
backend/agents/client/client.go
Normal file
@ -0,0 +1,307 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ResponseFormat struct {
|
||||
Type string `json:"type"`
|
||||
JsonSchema any `json:"json_schema,omitzero"`
|
||||
}
|
||||
|
||||
type AgentRequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
ResponseFormat ResponseFormat `json:"response_format"`
|
||||
|
||||
Tools *any `json:"tools,omitempty"`
|
||||
ToolChoice *string `json:"tool_choice,omitempty"`
|
||||
|
||||
RandomSeed *int `json:"random_seed,omitempty"`
|
||||
|
||||
EndToolCall string `json:"-"`
|
||||
|
||||
Chat *Chat `json:"messages"`
|
||||
}
|
||||
|
||||
func (req AgentRequestBody) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
Model string `json:"model"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
ResponseFormat ResponseFormat `json:"response_format"`
|
||||
|
||||
Tools *any `json:"tools,omitempty"`
|
||||
ToolChoice *string `json:"tool_choice,omitempty"`
|
||||
Messages []ChatMessage `json:"messages"`
|
||||
}{
|
||||
Model: req.Model,
|
||||
Temperature: req.Temperature,
|
||||
ResponseFormat: req.ResponseFormat,
|
||||
|
||||
Tools: req.Tools,
|
||||
ToolChoice: req.ToolChoice,
|
||||
|
||||
Messages: req.Chat.Messages,
|
||||
})
|
||||
}
|
||||
|
||||
type ResponseChoice struct {
|
||||
Index int `json:"index"`
|
||||
Message ChatAiMessage `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type AgentResponse struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Choices []ResponseChoice `json:"choices"`
|
||||
Created int `json:"created"`
|
||||
}
|
||||
|
||||
type AgentClient struct {
|
||||
url string
|
||||
apiKey string
|
||||
responseFormat string
|
||||
|
||||
ToolHandler ToolsHandlers
|
||||
|
||||
Log *log.Logger
|
||||
|
||||
Reply string
|
||||
|
||||
Do func(req *http.Request) (*http.Response, error)
|
||||
|
||||
Options CreateAgentClientOptions
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY = "REAL_OPEN_AI_KEY"
|
||||
|
||||
type CreateAgentClientOptions struct {
|
||||
Log *log.Logger
|
||||
SystemPrompt string
|
||||
JsonTools string
|
||||
EndToolCall string
|
||||
Query *string
|
||||
}
|
||||
|
||||
func CreateAgentClient(options CreateAgentClientOptions) AgentClient {
|
||||
apiKey := os.Getenv(OPENAI_API_KEY)
|
||||
|
||||
if len(apiKey) == 0 {
|
||||
panic("No api key")
|
||||
}
|
||||
|
||||
return AgentClient{
|
||||
apiKey: apiKey,
|
||||
url: "https://router.requesty.ai/v1/chat/completions",
|
||||
Do: func(req *http.Request) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
return client.Do(req)
|
||||
},
|
||||
|
||||
Log: options.Log,
|
||||
|
||||
ToolHandler: ToolsHandlers{
|
||||
handlers: map[string]ToolHandler{},
|
||||
},
|
||||
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
||||
req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+client.apiKey)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
|
||||
jsonAiRequest, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
|
||||
}
|
||||
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
|
||||
}
|
||||
|
||||
agentResponse := AgentResponse{}
|
||||
err = json.Unmarshal(response, &agentResponse)
|
||||
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
|
||||
}
|
||||
|
||||
if len(agentResponse.Choices) != 1 {
|
||||
client.Log.Errorf("Received more than 1 choice from AI \n %s\n", string(response))
|
||||
return AgentResponse{}, errors.New("Unsupported. We currently only accept 1 choice from AI.")
|
||||
}
|
||||
|
||||
msg := agentResponse.Choices[0].Message
|
||||
req.Chat.AddAiResponse(msg)
|
||||
|
||||
return agentResponse, nil
|
||||
}
|
||||
|
||||
func (client *AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
for {
|
||||
response, err := client.Request(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.Choices[0].FinishReason == "stop" {
|
||||
client.Log.Debug("Agent is finished")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = client.Process(info, req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
if err == FinishedCall {
|
||||
client.Log.Debug("Agent is finished")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var FinishedCall = errors.New("Last tool tool was called")
|
||||
|
||||
func (client *AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
var err error
|
||||
|
||||
message, err := req.Chat.GetLatest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aiMessage, ok := message.(ChatAiMessage)
|
||||
if !ok {
|
||||
return errors.New("Latest message isnt an AI message")
|
||||
}
|
||||
|
||||
if aiMessage.ToolCalls == nil {
|
||||
// Not an error, we just dont have any tool calls to process.
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, toolCall := range *aiMessage.ToolCalls {
|
||||
if toolCall.Function.Name == req.EndToolCall {
|
||||
return FinishedCall
|
||||
}
|
||||
|
||||
toolResponse := client.ToolHandler.Handle(info, toolCall)
|
||||
|
||||
if toolCall.Function.Name == "reply" {
|
||||
client.Reply = toolCall.Function.Arguments
|
||||
}
|
||||
|
||||
client.Log.Debug("Tool call", "name", toolCall.Function.Name, "arguments", toolCall.Function.Arguments, "response", toolResponse.Content)
|
||||
|
||||
req.Chat.AddToolResponse(toolResponse)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
|
||||
var tools any
|
||||
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
toolChoice := "auto"
|
||||
seed := 42
|
||||
|
||||
request := AgentRequestBody{
|
||||
Tools: &tools,
|
||||
ToolChoice: &toolChoice,
|
||||
Model: "policy/images",
|
||||
RandomSeed: &seed,
|
||||
Temperature: 0.3,
|
||||
EndToolCall: client.Options.EndToolCall,
|
||||
ResponseFormat: ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &Chat{
|
||||
Messages: make([]ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, client.Options.Query)
|
||||
|
||||
toolHandlerInfo := ToolHandlerInfo{
|
||||
ImageID: imageId,
|
||||
ImageName: imageName,
|
||||
UserId: userId,
|
||||
Image: &imageData,
|
||||
}
|
||||
|
||||
return client.ToolLoop(toolHandlerInfo, &request)
|
||||
}
|
||||
|
||||
func (client *AgentClient) RunAgentAlone(userID uuid.UUID, userReq string) error {
|
||||
var tools any
|
||||
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toolChoice := "auto"
|
||||
seed := 42
|
||||
|
||||
request := AgentRequestBody{
|
||||
Tools: &tools,
|
||||
ToolChoice: &toolChoice,
|
||||
Model: "policy/images",
|
||||
RandomSeed: &seed,
|
||||
Temperature: 0.3,
|
||||
EndToolCall: client.Options.EndToolCall,
|
||||
ResponseFormat: ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &Chat{
|
||||
Messages: make([]ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||
request.Chat.AddUser(userReq)
|
||||
|
||||
toolHandlerInfo := ToolHandlerInfo{
|
||||
UserId: userID,
|
||||
}
|
||||
|
||||
return client.ToolLoop(toolHandlerInfo, &request)
|
||||
}
|
||||
74
backend/agents/client/tools.go
Normal file
74
backend/agents/client/tools.go
Normal file
@ -0,0 +1,74 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ToolHandlerInfo struct {
|
||||
UserId uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
|
||||
// Pointer because we don't want to copy this around too much.
|
||||
Image *[]byte
|
||||
}
|
||||
|
||||
type ToolHandler struct {
|
||||
Fn func(info ToolHandlerInfo, args string, call ToolCall) (string, error)
|
||||
}
|
||||
|
||||
type ToolsHandlers struct {
|
||||
handlers map[string]ToolHandler
|
||||
}
|
||||
|
||||
var NoToolCallError = errors.New("An assistant tool call with no tool calls was provided.")
|
||||
|
||||
const NonExistantTool = "This tool does not exist"
|
||||
|
||||
func (handler ToolsHandlers) Handle(info ToolHandlerInfo, toolCallMessage ToolCall) ChatUserToolResponse {
|
||||
fnName := toolCallMessage.Function.Name
|
||||
arguments := toolCallMessage.Function.Arguments
|
||||
|
||||
responseMessage := ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Name: fnName,
|
||||
ToolCallId: toolCallMessage.Id,
|
||||
}
|
||||
|
||||
fnHandler, exists := handler.handlers[fnName]
|
||||
if !exists {
|
||||
responseMessage.Content = NonExistantTool
|
||||
return responseMessage
|
||||
}
|
||||
|
||||
res, err := fnHandler.Fn(info, arguments, toolCallMessage)
|
||||
|
||||
if err != nil {
|
||||
responseMessage.Content = err.Error()
|
||||
} else {
|
||||
responseMessage.Content = res
|
||||
}
|
||||
|
||||
return responseMessage
|
||||
}
|
||||
|
||||
func (handler *ToolsHandlers) AddTool(name string, fn func(info ToolHandlerInfo, args string, call ToolCall) (any, error)) {
|
||||
handler.handlers[name] = ToolHandler{
|
||||
Fn: func(info ToolHandlerInfo, args string, call ToolCall) (string, error) {
|
||||
res, err := fn(info, args, call)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
marshalledRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(marshalledRes), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
191
backend/agents/client/tools_test.go
Normal file
191
backend/agents/client/tools_test.go
Normal file
@ -0,0 +1,191 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ToolTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
handler ToolsHandlers
|
||||
client AgentClient
|
||||
}
|
||||
|
||||
func (suite *ToolTestSuite) SetupTest() {
|
||||
suite.handler = ToolsHandlers{
|
||||
handlers: map[string]ToolHandler{},
|
||||
}
|
||||
|
||||
suite.handler.AddTool("a", func(info ToolHandlerInfo, args string, call ToolCall) (any, error) {
|
||||
return args, nil
|
||||
})
|
||||
|
||||
suite.handler.AddTool("error", func(info ToolHandlerInfo, args string, call ToolCall) (any, error) {
|
||||
return false, errors.New("I will always error")
|
||||
})
|
||||
|
||||
suite.client.Log = log.New(os.Stdout)
|
||||
suite.client.ToolHandler = suite.handler
|
||||
}
|
||||
|
||||
func (suite *ToolTestSuite) TestSingleToolCall() {
|
||||
require := suite.Require()
|
||||
|
||||
response := suite.handler.Handle(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
ToolCall{
|
||||
Index: 0,
|
||||
Id: "1",
|
||||
Function: FunctionCall{
|
||||
Name: "a",
|
||||
Arguments: "return",
|
||||
},
|
||||
})
|
||||
|
||||
require.EqualValues(response, ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "\"return\"",
|
||||
ToolCallId: "1",
|
||||
Name: "a",
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *ToolTestSuite) TestMultipleToolCalls() {
|
||||
assert := suite.Assert()
|
||||
require := suite.Require()
|
||||
|
||||
chat := Chat{
|
||||
Messages: []ChatMessage{ChatAiMessage{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
ToolCalls: &[]ToolCall{
|
||||
{
|
||||
Index: 0,
|
||||
Id: "1",
|
||||
Function: FunctionCall{
|
||||
Name: "a",
|
||||
Arguments: "first-call",
|
||||
},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Id: "2",
|
||||
Function: FunctionCall{
|
||||
Name: "a",
|
||||
Arguments: "second-call",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := suite.client.Process(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
&AgentRequestBody{
|
||||
Chat: &chat,
|
||||
})
|
||||
|
||||
require.NoError(err, "Tool call shouldnt return an error")
|
||||
assert.EqualValues(chat.Messages[1:], []ChatMessage{
|
||||
ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "\"first-call\"",
|
||||
ToolCallId: "1",
|
||||
Name: "a",
|
||||
},
|
||||
ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "\"second-call\"",
|
||||
ToolCallId: "2",
|
||||
Name: "a",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *ToolTestSuite) TestMultipleToolCallsWithErrors() {
|
||||
assert := suite.Assert()
|
||||
require := suite.Require()
|
||||
|
||||
chat := Chat{
|
||||
Messages: []ChatMessage{ChatAiMessage{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
ToolCalls: &[]ToolCall{
|
||||
{
|
||||
Index: 0,
|
||||
Id: "1",
|
||||
Function: FunctionCall{
|
||||
Name: "error",
|
||||
Arguments: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Id: "2",
|
||||
Function: FunctionCall{
|
||||
Name: "non-existant",
|
||||
Arguments: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Id: "3",
|
||||
Function: FunctionCall{
|
||||
Name: "a",
|
||||
Arguments: "no-error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := suite.client.Process(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
&AgentRequestBody{
|
||||
Chat: &chat,
|
||||
})
|
||||
|
||||
require.NoError(err, "Tool call shouldnt return an error")
|
||||
|
||||
assert.EqualValues(chat.Messages[1:], []ChatMessage{
|
||||
ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "I will always error",
|
||||
ToolCallId: "1",
|
||||
Name: "error",
|
||||
},
|
||||
ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "This tool does not exist",
|
||||
ToolCallId: "2",
|
||||
Name: "non-existant",
|
||||
},
|
||||
ChatUserToolResponse{
|
||||
Role: "tool",
|
||||
Content: "\"no-error\"",
|
||||
ToolCallId: "3",
|
||||
Name: "a",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestToolSuite(t *testing.T) {
|
||||
suite.Run(t, &ToolTestSuite{
|
||||
client: AgentClient{},
|
||||
})
|
||||
}
|
||||
157
backend/agents/create_list_agent.go
Normal file
157
backend/agents/create_list_agent.go
Normal file
@ -0,0 +1,157 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createListAgentPrompt = `
|
||||
You are an agent who's job is to produce a reasonable output for an unstructured input.
|
||||
|
||||
Your job is to create lists for the user, the user will give you a title and some fields they want
|
||||
as part of the list. Your job is to take these fields, adjust their names so they have good names,
|
||||
and add a good description for each one.
|
||||
|
||||
You can add fields if you think they make a lot of sense.
|
||||
You can remove fields if they are not correct, but be sure before you do this.
|
||||
|
||||
You must respond in json format, do not add backticks to the json. ONLY valid json.
|
||||
`
|
||||
|
||||
const listJsonSchema = `
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "the title of the list"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "the description of the list"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the field."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the field."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"description": "An array of field objects."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
type createNewListArguments struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
|
||||
Fields []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"fields"`
|
||||
}
|
||||
|
||||
type CreateListAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
stackModel models.StackModel
|
||||
}
|
||||
|
||||
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stackID uuid.UUID, title string, userReq string) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "policy/images",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "json_schema",
|
||||
JsonSchema: listJsonSchema,
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
|
||||
|
||||
req := fmt.Sprintf("List title: %s | Users list description: %s", title, userReq)
|
||||
|
||||
request.Chat.AddUser(req)
|
||||
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
content := resp.Choices[0].Message.Content
|
||||
|
||||
if strings.HasPrefix(content, "```json") {
|
||||
content = content[len("```json") : len(content)-3]
|
||||
}
|
||||
|
||||
log.Info("", "res", content)
|
||||
|
||||
var createListArgs createNewListArguments
|
||||
err = json.Unmarshal([]byte(content), &createListArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaItems := make([]model.SchemaItems, 0)
|
||||
for _, field := range createListArgs.Fields {
|
||||
schemaItems = append(schemaItems, model.SchemaItems{
|
||||
StackID: stackID,
|
||||
|
||||
Item: field.Name,
|
||||
Description: field.Description,
|
||||
|
||||
Value: "string", // keep it simple for now.
|
||||
})
|
||||
}
|
||||
|
||||
err = agent.stackModel.SaveItems(ctx, schemaItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating list agent, saving items: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCreateListAgent(log *log.Logger, listModel models.StackModel) CreateListAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: createListAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := CreateListAgent{
|
||||
client,
|
||||
listModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
||||
74
backend/agents/description_agent.go
Normal file
74
backend/agents/description_agent.go
Normal file
@ -0,0 +1,74 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const noteAgentPrompt = `
|
||||
You are an AI agent who's job is to describe the image you see.
|
||||
|
||||
You should also add any text you see in the image, if no text exists, just add a description.
|
||||
Be consise and don't add too much extra information or formatting characters, simple text.
|
||||
|
||||
You must write this text in Markdown. You can add extra information for the user.
|
||||
You must organise this text nicely, not be all over the place.
|
||||
`
|
||||
|
||||
type DescriptionAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
imageModel models.ImageModel
|
||||
}
|
||||
|
||||
func (agent DescriptionAgent) Describe(log *log.Logger, imageID uuid.UUID, imageName string, imageData []byte) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "policy/images",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(noteAgentPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, nil)
|
||||
|
||||
log.Debug("Sending description request")
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not request. %s", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
description := resp.Choices[0].Message.Content
|
||||
|
||||
err = agent.imageModel.UpdateDescription(ctx, imageID, description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDescriptionAgent(log *log.Logger, imageModel models.ImageModel) DescriptionAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: noteAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := DescriptionAgent{
|
||||
client: client,
|
||||
imageModel: imageModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
||||
259
backend/agents/list_agent.go
Normal file
259
backend/agents/list_agent.go
Normal file
@ -0,0 +1,259 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const listPrompt = `
|
||||
**You are an AI used to classify what list a certain image belongs in**
|
||||
|
||||
You will need to decide using tool calls, if you must create a new list, or use an existing one.
|
||||
You must be specific enough so it is useful, but not too specific such that all images belong on seperate lists.
|
||||
|
||||
An example of lists are:
|
||||
- Locations
|
||||
- Events
|
||||
- TV Shows
|
||||
- Movies
|
||||
- Books
|
||||
|
||||
Another one of your tasks is to create a schema for this list. This should contain information that this, and following
|
||||
pictures contain. Be specific but also generic. You should use the parameters in "createList" to create this schema.
|
||||
|
||||
This schema should not be super specific. You must be able to understand the image, and if the content of the image doesnt seem relevant, try
|
||||
and extract some meaning about what the image is.
|
||||
|
||||
You must call "listLists" to see which available lists are already available.
|
||||
Use "createList" only once, don't create multiple lists for one image.
|
||||
|
||||
You can add an image to multiple lists, this is also true if you already created a list. But only add to a list if it makes sense to do so.
|
||||
|
||||
**Tools:**
|
||||
* think: Internal reasoning/planning step.
|
||||
* listLists: Get existing lists
|
||||
* createList: Creates a new list with a name and description. Only use this once.
|
||||
* addToList: Add to an existing list. This will also mean extracting information from this image, and inserting it, fitting the schema.
|
||||
* stopAgent: Signal task completion.
|
||||
`
|
||||
|
||||
const listTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use this tool to think through the image, evaluating the event and whether or not it exists in the users listEvents.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image."
|
||||
}
|
||||
},
|
||||
"required": ["thought"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listLists",
|
||||
"description": "Retrieves the list of the user's existing lists.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createList",
|
||||
"description": "Creates a new list",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of this new list."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A simple description of this list."
|
||||
},
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Item": {
|
||||
"type": "string",
|
||||
"description": "The name of the key for this specific field. Similar to a column in a database"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string",
|
||||
"enum": ["string", "number", "boolean"]
|
||||
},
|
||||
"Description": {
|
||||
"type": "string",
|
||||
"description": "The description for this item"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "schema"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "addToList",
|
||||
"description": "Adds an image to a list, this could be a new one you just created or not.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the existing list"
|
||||
},
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A key-value of ID - value from this image to fit the schema. any of the values can be null",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the schema item."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "the concrete value for this field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["listId", "schema"]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stopAgent",
|
||||
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type createListArguments struct {
|
||||
Name string `json:"name"`
|
||||
Desription string `json:"description"`
|
||||
Schema []model.SchemaItems
|
||||
}
|
||||
type addToListArguments struct {
|
||||
ListID string `json:"listId"`
|
||||
Schema []models.IDValue
|
||||
}
|
||||
|
||||
func NewStackAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: listPrompt,
|
||||
JsonTools: listTools,
|
||||
Log: log,
|
||||
EndToolCall: "stopAgent",
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createListArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hasReachedLimit, err := limitsMethods.HasReachedStackLimit(info.UserId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error checking stack limits: %w", err)
|
||||
}
|
||||
|
||||
if hasReachedLimit {
|
||||
log.Warn("User has reached limits", "userID", info.UserId)
|
||||
return "", fmt.Errorf("reached stack limits")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
savedList, err := stackModel.Save(ctx, info.UserId, args.Name, args.Desription, model.Progress_Complete)
|
||||
if err != nil {
|
||||
log.Error("saving list", "err", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range args.Schema {
|
||||
args.Schema[i].StackID = savedList.ID
|
||||
}
|
||||
|
||||
err = stackModel.SaveItems(ctx, args.Schema)
|
||||
if err != nil {
|
||||
log.Error("saving items", "err", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return savedList, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return stackModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := addToListArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
listUUID, err := uuid.Parse(args.ListID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imageStack, err := stackModel.SaveImage(ctx, info.ImageID, listUUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := stackModel.SaveSchemaItems(ctx, imageStack.ID, args.Schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
||||
68
backend/auth/auth.go
Normal file
68
backend/auth/auth.go
Normal file
@ -0,0 +1,68 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Code string
|
||||
Valid time.Time
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
codes map[string]Code
|
||||
|
||||
mailer Mailer
|
||||
}
|
||||
|
||||
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (a *Auth) CreateCode(email string) error {
|
||||
code := randString(10)
|
||||
|
||||
if err := a.mailer.SendCode(email, code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.codes[email] = Code{
|
||||
Code: code,
|
||||
Valid: time.Now().Add(time.Minute),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auth) IsCodeValid(email string, code string) bool {
|
||||
existingCode, exists := a.codes[email]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return existingCode.Valid.After(time.Now()) && existingCode.Code == code
|
||||
}
|
||||
|
||||
func (a *Auth) UseCode(email string, code string) error {
|
||||
if valid := a.IsCodeValid(email, code); !valid {
|
||||
return errors.New("This code is invalid.")
|
||||
}
|
||||
|
||||
delete(a.codes, email)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateAuth(mailer Mailer) Auth {
|
||||
return Auth{
|
||||
codes: make(map[string]Code),
|
||||
mailer: mailer,
|
||||
}
|
||||
}
|
||||
30
backend/auth/auth_test.go
Normal file
30
backend/auth/auth_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestMail struct{}
|
||||
|
||||
func (m TestMail) SendCode(to string, code string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var testMailer = TestMail{}
|
||||
|
||||
func TestCreateCode(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
auth := CreateAuth(testMailer)
|
||||
|
||||
err := auth.CreateCode("test")
|
||||
require.NoError(err)
|
||||
|
||||
code, exists := auth.codes["test"]
|
||||
require.True(exists)
|
||||
require.True(code.Valid.After(time.Now()))
|
||||
require.True(auth.IsCodeValid("test", code.Code))
|
||||
}
|
||||
76
backend/auth/email.go
Normal file
76
backend/auth/email.go
Normal file
@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
type MailClient struct {
|
||||
client *mail.Client
|
||||
}
|
||||
|
||||
type TestMailClient struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Mailer interface {
|
||||
SendCode(to string, code string) error
|
||||
}
|
||||
|
||||
func (m MailClient) getMessage() (*mail.Msg, error) {
|
||||
message := mail.NewMsg()
|
||||
if err := message.From("auth@johncosta.tech"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (m MailClient) SendCode(to string, code string) error {
|
||||
msg, err := m.getMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := msg.To(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Subject("Login to Haystack")
|
||||
msg.SetBodyString(mail.TypeTextPlain, code)
|
||||
|
||||
return m.client.DialAndSend(msg)
|
||||
}
|
||||
|
||||
func (m TestMailClient) SendCode(to string, code string) error {
|
||||
m.logger.Info("Auth Code", "email", to, "code", code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||
mode := os.Getenv("MODE")
|
||||
if mode == "DEV" {
|
||||
return TestMailClient{
|
||||
log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := mail.NewClient(
|
||||
"smtp.mailbox.org",
|
||||
mail.WithTLSPortPolicy(mail.TLSMandatory),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthPlain),
|
||||
mail.WithUsername(os.Getenv("EMAIL_USERNAME")),
|
||||
mail.WithPassword(os.Getenv("EMAIL_PASSWORD")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return MailClient{}, err
|
||||
}
|
||||
|
||||
return MailClient{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
135
backend/auth/handler.go
Normal file
135
backend/auth/handler.go
Normal file
@ -0,0 +1,135 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
user models.UserModel
|
||||
|
||||
auth Auth
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
}
|
||||
|
||||
type loginBody struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type codeBody struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type codeReturn struct {
|
||||
Access string `json:"access"`
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
|
||||
type refreshBody struct {
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
|
||||
type refreshReturn struct {
|
||||
Access string `json:"access"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
||||
err := h.auth.CreateCode(body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) code(body codeBody, w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.auth.UseCode(body.Email, body.Code); err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "email or code are incorrect", w)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: we should only keep emails around for a little bit.
|
||||
// Time to first login should be less than 10 minutes.
|
||||
// So actually, they shouldn't be written to our database.
|
||||
if exists := h.user.DoesUserExist(r.Context(), body.Email); !exists {
|
||||
h.user.Save(r.Context(), model.Users{
|
||||
Email: body.Email,
|
||||
})
|
||||
}
|
||||
|
||||
uuid, err := h.user.GetUserIdFromEmail(r.Context(), body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "failed to get user", w)
|
||||
return
|
||||
}
|
||||
|
||||
refresh := h.jwtManager.CreateRefreshToken(uuid)
|
||||
access := h.jwtManager.CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := codeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, codeReturn, w)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) refresh(body refreshBody, w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("token", "refresh", body.Refresh)
|
||||
userId, err := h.jwtManager.GetUserIdFromRefresh(body.Refresh)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "invalid refresh token: "+err.Error(), w)
|
||||
return
|
||||
}
|
||||
|
||||
access := h.jwtManager.CreateAccessToken(userId)
|
||||
|
||||
refreshReturn := refreshReturn{
|
||||
Access: access,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, refreshReturn, w)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting auth router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Post("/login", middleware.WithValidatedPost(h.login))
|
||||
r.Post("/code", middleware.WithValidatedPost(h.code))
|
||||
r.Post("/refresh", middleware.WithValidatedPost(h.refresh))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateAuthHandler(db *sql.DB, jwtManager *middleware.JwtManager) AuthHandler {
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Auth")
|
||||
|
||||
mailer, err := CreateMailClient(logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
auth := CreateAuth(mailer)
|
||||
|
||||
return AuthHandler{
|
||||
logger: logger,
|
||||
user: userModel,
|
||||
auth: auth,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
31
backend/docker-compose.yml
Normal file
31
backend/docker-compose.yml
Normal file
@ -0,0 +1,31 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: haystack
|
||||
volumes:
|
||||
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
- ./pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 4321:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d haystack"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
timeout: 5s
|
||||
backend:
|
||||
build: .
|
||||
restart: always
|
||||
env_file: .env.docker
|
||||
ports:
|
||||
- 3040:3040
|
||||
volumes:
|
||||
- ./.env.docker:/app/.env
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
81
backend/events.go
Normal file
81
backend/events.go
Normal file
@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/notifications"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
* TODO: We have channels open every a user sends an image.
|
||||
* We never close these channels.
|
||||
*
|
||||
* What is a reasonable default? Close the channel after 1 minute of inactivity?
|
||||
*/
|
||||
func CreateEventsHandler(notifier *notifications.Notifier[notifications.Notification]) http.HandlerFunc {
|
||||
counter := 0
|
||||
|
||||
userSplitters := make(map[string]*notifications.ChannelSplitter[notifications.Notification])
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_userId := r.Context().Value(middleware.USER_ID).(uuid.UUID)
|
||||
if _userId == uuid.Nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId := _userId.String()
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
// w.(http.Flusher).Flush()
|
||||
|
||||
if _, exists := notifier.Listeners[userId]; !exists {
|
||||
notifier.Create(userId)
|
||||
}
|
||||
|
||||
userNotifications := notifier.Listeners[userId]
|
||||
|
||||
if _, exists := userSplitters[userId]; !exists {
|
||||
splitter := notifications.NewChannelSplitter(userNotifications)
|
||||
|
||||
userSplitters[userId] = &splitter
|
||||
splitter.Listen()
|
||||
}
|
||||
|
||||
splitter := userSplitters[userId]
|
||||
|
||||
id := strconv.Itoa(counter)
|
||||
counter += 1
|
||||
|
||||
notifications := splitter.Add(id)
|
||||
defer splitter.Remove(id)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
fmt.Fprint(w, "event: close\ndata: Connection closed\n\n")
|
||||
w.(http.Flusher).Flush()
|
||||
return
|
||||
case msg := <-notifications:
|
||||
|
||||
msgString, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Sending msg %s\n", msgString)
|
||||
|
||||
fmt.Fprintf(w, "event: data\ndata: %s\n\n", string(msgString))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
backend/go.mod
Normal file
31
backend/go.mod
Normal file
@ -0,0 +1,31 @@
|
||||
module screenmark/screenmark
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.0.0 // indirect
|
||||
github.com/charmbracelet/log v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.4.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-jet/jet/v2 v2.13.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robert-nix/ansihtml v1.0.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/wneessen/go-mail v0.6.2 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
121
backend/go.sum
Normal file
121
backend/go.sum
Normal file
@ -0,0 +1,121 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
|
||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
||||
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
|
||||
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
|
||||
github.com/go-jet/jet/v2 v2.13.0 h1:DcD2IJRGos+4X40IQRV6S6q9onoOfZY/GPdvU6ImZcQ=
|
||||
github.com/go-jet/jet/v2 v2.13.0/go.mod h1:YhT75U1FoYAxFOObbQliHmXVYQeffkBKWT7ZilZ3zPc=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
|
||||
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
232
backend/images/handler.go
Normal file
232
backend/images/handler.go
Normal file
@ -0,0 +1,232 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/processor"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
imageModel models.ImageModel
|
||||
userModel models.UserModel
|
||||
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
|
||||
processor *processor.Processor[model.Image]
|
||||
}
|
||||
|
||||
type ImagesReturn struct {
|
||||
UserImages []models.UserImageWithImage
|
||||
Stacks []models.ListsWithImages
|
||||
}
|
||||
|
||||
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageID, err := middleware.GetPathParamID(h.logger, "id", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
image, exists, err := h.imageModel.Get(r.Context(), imageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Could not get image")
|
||||
return
|
||||
}
|
||||
|
||||
// Do not leak that this ID exists.
|
||||
if !exists || image.UserID != userID {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this could be part of the db table
|
||||
extension := filepath.Ext(image.ImageName)
|
||||
if len(extension) == 0 {
|
||||
// Same hack
|
||||
extension = "png"
|
||||
}
|
||||
extension = extension[1:]
|
||||
|
||||
w.Header().Add("Content-Type", "image/"+extension)
|
||||
w.Write(image.Image)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
images, err := h.userModel.GetUserImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get user images", w)
|
||||
return
|
||||
}
|
||||
|
||||
stacksWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
|
||||
return
|
||||
}
|
||||
|
||||
imagesReturn := ImagesReturn{
|
||||
UserImages: images,
|
||||
Stacks: stacksWithImages,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, imagesReturn, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageName := chi.URLParam(r, "name")
|
||||
if len(imageName) == 0 {
|
||||
middleware.WriteErrorBadRequest(h.logger, "you need to provide a name in the path", w)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
image := make([]byte, 0)
|
||||
switch contentType {
|
||||
case "application/base64":
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
_, err := io.Copy(buf, decoder)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "base64 decoding failed", w)
|
||||
return
|
||||
}
|
||||
|
||||
image = buf.Bytes()
|
||||
case "application/oclet-stream", "image/png":
|
||||
bodyData, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "binary data reading failed", w)
|
||||
return
|
||||
}
|
||||
// TODO: check headers
|
||||
|
||||
image = bodyData
|
||||
default:
|
||||
middleware.WriteErrorBadRequest(h.logger, "unsupported content type, need octet-stream or base64", w)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
newImage, err := h.imageModel.Save(ctx, imageName, image, userID)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not save image to DB: "+err.Error(), w)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("About to add image")
|
||||
h.processor.Add(newImage)
|
||||
|
||||
// We nullify the image's data, so we're not transferring all that
|
||||
// data back to the frontend.
|
||||
newImage.Image = nil
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, newImage, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) {
|
||||
stringImageID := chi.URLParam(r, "image-id")
|
||||
imageID, err := uuid.Parse(stringImageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := h.imageModel.Delete(ctx, imageID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("cannot delete image", "error", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't leak if the image exists or not
|
||||
if !exists {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting image router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRouteURL(h.jwtManager))
|
||||
r.Get("/{id}", h.serveImage)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute(h.jwtManager))
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.listImages)
|
||||
r.Post("/{name}", middleware.WithLimit(h.logger, h.limitsManager.HasReachedImageLimit, h.uploadImage))
|
||||
r.Delete("/{image-id}", h.deleteImage)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateImageHandler(
|
||||
db *sql.DB,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
jwtManager *middleware.JwtManager,
|
||||
processor *processor.Processor[model.Image],
|
||||
) ImageHandler {
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Images")
|
||||
|
||||
return ImageHandler{
|
||||
logger: logger,
|
||||
imageModel: imageModel,
|
||||
userModel: userModel,
|
||||
limitsManager: limitsManager,
|
||||
jwtManager: jwtManager,
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
803
backend/integration_test.go
Normal file
803
backend/integration_test.go
Normal file
@ -0,0 +1,803 @@
|
||||
// Integration Tests for Haystack Backend
|
||||
//
|
||||
// These tests provide comprehensive end-to-end testing of all API endpoints.
|
||||
//
|
||||
// Requirements:
|
||||
// - Docker must be installed and running
|
||||
// - PostgreSQL Docker image will be automatically pulled and started
|
||||
//
|
||||
// To run the integration tests:
|
||||
//
|
||||
// 1. Start Docker daemon
|
||||
// 2. Run: go test -v ./integration_test.go
|
||||
//
|
||||
// The tests will:
|
||||
// - Start a PostgreSQL container on port 5433
|
||||
// - Set up the database schema
|
||||
// - Test all auth, stack, and image endpoints
|
||||
// - Clean up the container after tests complete
|
||||
//
|
||||
// Note: These tests require Docker and will be skipped if Docker is not available.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
testDBName = "test_haystack"
|
||||
testDBUser = "test_user"
|
||||
testDBPassword = "test_password"
|
||||
testDBHost = "localhost"
|
||||
testDBPort = "5433"
|
||||
testDBSSLMode = "disable"
|
||||
)
|
||||
|
||||
type TestUser struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
Token string
|
||||
}
|
||||
|
||||
type TestContext struct {
|
||||
db *sql.DB
|
||||
router chi.Router
|
||||
server *httptest.Server
|
||||
users []TestUser
|
||||
cleanup func()
|
||||
jwtManager *middleware.JwtManager
|
||||
}
|
||||
|
||||
func setupTestDatabase() (*sql.DB, func(), error) {
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
return nil, nil, fmt.Errorf("docker daemon is not running: %w", err)
|
||||
}
|
||||
|
||||
// Start PostgreSQL container
|
||||
containerName := "test_postgres_haystack"
|
||||
|
||||
// Clean up any existing container
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
|
||||
// Start new PostgreSQL container
|
||||
cmd := exec.Command("docker", "run", "-d",
|
||||
"--name", containerName,
|
||||
"-e", "POSTGRES_DB="+testDBName,
|
||||
"-e", "POSTGRES_USER="+testDBUser,
|
||||
"-e", "POSTGRES_PASSWORD="+testDBPassword,
|
||||
"-p", testDBPort+":5432",
|
||||
"postgres:15-alpine",
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start postgres container: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Wait for database to be ready with retries
|
||||
maxRetries := 15
|
||||
for i := range maxRetries {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Test connection
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
testDB, testErr := sql.Open("postgres", connStr)
|
||||
if testErr == nil {
|
||||
if pingErr := testDB.Ping(); pingErr == nil {
|
||||
testDB.Close()
|
||||
break
|
||||
}
|
||||
testDB.Close()
|
||||
}
|
||||
|
||||
if i == maxRetries-1 {
|
||||
return nil, nil, fmt.Errorf("database failed to become ready after %d retries", maxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to connect to test database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to ping test database: %w", err)
|
||||
}
|
||||
|
||||
// Load and execute schema
|
||||
schema, err := os.ReadFile("schema.sql")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read schema file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(string(schema)); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute schema: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
cleanup := func() {
|
||||
db.Close()
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
}
|
||||
|
||||
return db, cleanup, nil
|
||||
}
|
||||
|
||||
func setupTestContext(t *testing.T) *TestContext {
|
||||
// Set environment variables for test environment
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
originalDBConn := os.Getenv("DB_CONNECTION")
|
||||
originalTestEnv := os.Getenv("GO_TEST_ENVIRONMENT")
|
||||
|
||||
os.Setenv("DB_CONNECTION", connStr)
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", "true")
|
||||
|
||||
defer func() {
|
||||
if originalDBConn != "" {
|
||||
os.Setenv("DB_CONNECTION", originalDBConn)
|
||||
} else {
|
||||
os.Unsetenv("DB_CONNECTION")
|
||||
}
|
||||
if originalTestEnv != "" {
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", originalTestEnv)
|
||||
} else {
|
||||
os.Unsetenv("GO_TEST_ENVIRONMENT")
|
||||
}
|
||||
}()
|
||||
|
||||
tc := &TestContext{}
|
||||
|
||||
db, cleanup, err := setupTestDatabase()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to setup test database: %v", err)
|
||||
}
|
||||
|
||||
jwtManager := middleware.NewJwtManager([]byte("test-jwt-secret"))
|
||||
router, err := setupRouter(db, jwtManager)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
tc.db = db
|
||||
tc.router = router
|
||||
tc.server = server
|
||||
tc.jwtManager = jwtManager
|
||||
tc.cleanup = func() {
|
||||
server.Close()
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *TestContext) createTestUser(email string) TestUser {
|
||||
// Insert user into database
|
||||
var userID uuid.UUID
|
||||
err := tc.db.QueryRow("INSERT INTO haystack.users (email) VALUES ($1) RETURNING id", email).Scan(&userID)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test user: %v", err))
|
||||
}
|
||||
|
||||
// Create access token for the user
|
||||
accessToken := tc.jwtManager.CreateAccessToken(userID)
|
||||
|
||||
user := TestUser{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Token: accessToken,
|
||||
}
|
||||
|
||||
tc.users = append(tc.users, user)
|
||||
return user
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeRequest(t *testing.T, method, path, token string, body io.Reader) *http.Response {
|
||||
url := tc.server.URL + path
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeJSONRequest(t *testing.T, method, path, token string, data any) *http.Response {
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
body = bytes.NewReader(jsonData)
|
||||
}
|
||||
|
||||
return tc.makeRequest(t, method, path, token, body)
|
||||
}
|
||||
|
||||
// Comprehensive integration test suite - single database setup for all tests
|
||||
func TestAllRoutes(t *testing.T) {
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Create test users for different test scenarios
|
||||
stackUser := tc.createTestUser("stacktest@example.com")
|
||||
imageUser := tc.createTestUser("imagetest@example.com")
|
||||
flowUser := tc.createTestUser("flowtest@example.com")
|
||||
|
||||
t.Run("Auth Routes", func(t *testing.T) {
|
||||
t.Run("Login endpoint", func(t *testing.T) {
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Code endpoint with valid email", func(t *testing.T) {
|
||||
// First create a login request to set up the email
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
|
||||
// Then try to use a code (this will fail with invalid code, but tests the endpoint)
|
||||
codeData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"code": "invalid",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/code", "", codeData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The auth system creates a user for new emails, so this returns 200
|
||||
// We're testing that the endpoint works, not necessarily the code validation
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for code endpoint, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Protected route without token", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for protected route without token, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Stack Routes", func(t *testing.T) {
|
||||
t.Run("Get stacks without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stacks with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create stack", func(t *testing.T) {
|
||||
stackData := map[string]string{
|
||||
"title": "Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stack items with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack without authentication", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for unauthenticated delete, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete non-existent stack", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for non-existent stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create and delete stack successfully", func(t *testing.T) {
|
||||
// First create a stack
|
||||
stackData := map[string]string{
|
||||
"title": "Stack to Delete",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for deletion test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list of stacks to find the created stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Errorf("No stacks found after creation")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the stack we just created
|
||||
var stackToDelete map[string]interface{}
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Stack to Delete" {
|
||||
stackToDelete = stack
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if stackToDelete == nil {
|
||||
t.Errorf("Could not find created stack")
|
||||
return
|
||||
}
|
||||
|
||||
stackID, ok := stackToDelete["ID"].(string)
|
||||
if !ok {
|
||||
t.Errorf("Stack ID not found or not a string")
|
||||
return
|
||||
}
|
||||
|
||||
// Now delete the stack
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for successful delete, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Verify the stack is gone by trying to get it again
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var stacksAfterDelete []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacksAfterDelete); err != nil {
|
||||
t.Errorf("Failed to decode stacks response after delete: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the deleted stack is no longer in the list
|
||||
for _, stack := range stacksAfterDelete {
|
||||
if id, ok := stack["ID"].(string); ok && id == stackID {
|
||||
t.Errorf("Stack still exists after deletion")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack belonging to different user", func(t *testing.T) {
|
||||
// Create a stack with stackUser
|
||||
stackData := map[string]string{
|
||||
"title": "Other User's Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for ownership test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var stackID string
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Other User's Stack" {
|
||||
if id, ok := stack["ID"].(string); ok {
|
||||
stackID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stackID == "" {
|
||||
t.Errorf("Could not find created stack ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to delete the stack with a different user (imageUser)
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 when deleting another user's stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Image Routes", func(t *testing.T) {
|
||||
t.Run("Get images without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get images with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with base64", func(t *testing.T) {
|
||||
// Create a simple valid base64 string for testing
|
||||
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test.png", strings.NewReader(testImageBase64))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "application/base64")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with binary data", func(t *testing.T) {
|
||||
// Create a small test image (minimal PNG)
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test2.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image without name", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "POST", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Route pattern doesn't match empty names, so returns 404
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for missing name, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Serve non-existent image", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "GET", "/images/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for non-existent image, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Complete User Flow", func(t *testing.T) {
|
||||
// Step 1: Test authentication is working
|
||||
resp := tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 2: Upload an image
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test_flow.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create upload request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+flowUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload image: %v", err)
|
||||
}
|
||||
|
||||
// The API returns 200 for successful image uploads
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Image upload failed, expected 200, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 3: Verify image appears in user's image list
|
||||
resp = tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode image list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check that we have user images
|
||||
if userImages, ok := imageData["userImages"].([]interface{}); ok {
|
||||
if len(userImages) == 0 {
|
||||
t.Log("Warning: No user images found, but upload succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d user images", len(userImages))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Test stack creation
|
||||
stackData := map[string]string{
|
||||
"title": "Integration Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp = tc.makeJSONRequest(t, "POST", "/stacks/", flowUser.Token, stackData)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Stack creation failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 5: Verify stack appears in user's stack list
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user stacks, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stack list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Log("Warning: No stacks found, but creation succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d stacks", len(stacks))
|
||||
}
|
||||
|
||||
t.Log("Complete user flow test passed!")
|
||||
})
|
||||
}
|
||||
|
||||
// Simple test that doesn't require Docker
|
||||
func TestIntegrationTestSetup(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
t.Run("Test structure validation", func(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
// Verify that our test types are properly defined
|
||||
var _ TestUser
|
||||
var _ TestContext
|
||||
|
||||
// Verify that our constants are defined
|
||||
if testDBName == "" {
|
||||
t.Error("testDBName constant is not defined")
|
||||
}
|
||||
|
||||
if testDBPort == "" {
|
||||
t.Error("testDBPort constant is not defined")
|
||||
}
|
||||
|
||||
t.Log("Test structure is valid")
|
||||
})
|
||||
|
||||
t.Run("Database and router setup", func(t *testing.T) {
|
||||
// This test verifies that the database and router can be set up without SSL errors
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Verify that the router was created successfully
|
||||
if tc.router == nil {
|
||||
t.Error("Router was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the server was created successfully
|
||||
if tc.server == nil {
|
||||
t.Error("Server was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the database connection is working
|
||||
if err := tc.db.Ping(); err != nil {
|
||||
t.Errorf("Database connection failed: %v", err)
|
||||
}
|
||||
|
||||
t.Log("Database and router setup successful - no SSL errors!")
|
||||
})
|
||||
|
||||
t.Run("Docker availability check", func(t *testing.T) {
|
||||
// Check if Docker is available but don't fail the test
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("Docker not found, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
t.Skip("Docker daemon is not running, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
t.Log("Docker is available and running")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Check if Docker is available
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
fmt.Println("Docker not found, skipping integration tests")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
fmt.Println("Docker daemon is not running, skipping integration tests")
|
||||
fmt.Println("To run integration tests, start Docker daemon and try again")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
61
backend/limits/limits.go
Normal file
61
backend/limits/limits.go
Normal file
@ -0,0 +1,61 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
LISTS_LIMIT = 10
|
||||
IMAGE_LIMIT = 10
|
||||
)
|
||||
|
||||
type LimitsManager struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type LimitsManagerMethods interface {
|
||||
HasReachedStackLimit(userID uuid.UUID) (bool, error)
|
||||
HasReachedImageLimit(userID uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type listCount struct {
|
||||
ListCount int `alias:"list_count"`
|
||||
}
|
||||
|
||||
func (m *LimitsManager) HasReachedStackLimit(userID uuid.UUID) (bool, error) {
|
||||
getStacks := Stacks.
|
||||
SELECT(COUNT(Stacks.UserID).AS("listCount.ListCount")).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userID)))
|
||||
|
||||
var count listCount
|
||||
err := getStacks.Query(m.dbPool, &count)
|
||||
|
||||
return count.ListCount >= LISTS_LIMIT, err
|
||||
}
|
||||
|
||||
type imageCount struct {
|
||||
ImageCount int `alias:"image_count"`
|
||||
}
|
||||
|
||||
func (m *LimitsManager) HasReachedImageLimit(userID uuid.UUID) (bool, error) {
|
||||
getStacks := Image.
|
||||
SELECT(COUNT(Image.UserID).AS("imageCount.ImageCount")).
|
||||
WHERE(Image.UserID.EQ(UUID(userID)))
|
||||
|
||||
var count imageCount
|
||||
err := getStacks.Query(m.dbPool, &count)
|
||||
|
||||
return count.ImageCount >= IMAGE_LIMIT, err
|
||||
}
|
||||
|
||||
func CreateLimitsManager(db *sql.DB) *LimitsManager {
|
||||
return &LimitsManager{
|
||||
db,
|
||||
}
|
||||
}
|
||||
54
backend/logs.go
Normal file
54
backend/logs.go
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
type DatabaseWriter struct {
|
||||
dbPool *sql.DB
|
||||
imageId uuid.UUID
|
||||
}
|
||||
|
||||
func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
|
||||
return &DatabaseWriter{
|
||||
dbPool: dbPool,
|
||||
imageId: imageId,
|
||||
}
|
||||
}
|
||||
|
||||
func createDbStdoutWriter(dbPool *sql.DB, imageId uuid.UUID) io.Writer {
|
||||
return io.MultiWriter(os.Stdout, newDatabaseWriter(dbPool, imageId))
|
||||
}
|
||||
|
||||
func createLogger(prefix string, writer io.Writer) *log.Logger {
|
||||
logger := log.NewWithOptions(writer, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: prefix,
|
||||
Formatter: log.TextFormatter,
|
||||
})
|
||||
|
||||
logger.SetColorProfile(termenv.TrueColor)
|
||||
logger.SetLevel(log.DebugLevel)
|
||||
|
||||
return logger
|
||||
}
|
||||
50
backend/main.go
Normal file
50
backend/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
panic("JWT_SECRET environment variable not set")
|
||||
}
|
||||
|
||||
jwtManager := middleware.NewJwtManager([]byte(jwtSecret))
|
||||
|
||||
db, err := models.InitDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
router, err := setupRouter(db, jwtManager)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
port, exists := os.LookupEnv("PORT")
|
||||
if !exists {
|
||||
panic("no port can be found")
|
||||
}
|
||||
|
||||
portWithColon := fmt.Sprintf(":%s", port)
|
||||
|
||||
logger := createLogger("Main", os.Stdout)
|
||||
|
||||
logger.Info("Serving router", "port", portWithColon)
|
||||
err = http.ListenAndServe(portWithColon, router)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
29
backend/middleware/body.go
Normal file
29
backend/middleware/body.go
Normal file
@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WithValidatedPost[K any](
|
||||
fn func(request K, w http.ResponseWriter, r *http.Request),
|
||||
) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
request := new(K)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, request)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fn(*request, w, r)
|
||||
}
|
||||
}
|
||||
11
backend/middleware/json.go
Normal file
11
backend/middleware/json.go
Normal file
@ -0,0 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func SetJson(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
136
backend/middleware/jwt.go
Normal file
136
backend/middleware/jwt.go
Normal file
@ -0,0 +1,136 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type JwtType string
|
||||
|
||||
const (
|
||||
Access JwtType = "access"
|
||||
Refresh JwtType = "refresh"
|
||||
)
|
||||
|
||||
type JwtClaims struct {
|
||||
UserID string
|
||||
Type JwtType
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type JwtManager struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
func NewJwtManager(secret []byte) *JwtManager {
|
||||
return &JwtManager{secret: secret}
|
||||
}
|
||||
|
||||
func (jm *JwtManager) createToken(claims JwtClaims) *jwt.Token {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"UserID": claims.UserID,
|
||||
"Type": claims.Type,
|
||||
"exp": claims.Expiry.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
func (jm *JwtManager) CreateRefreshToken(userId uuid.UUID) string {
|
||||
token := jm.createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Refresh,
|
||||
Expiry: time.Now().Add(time.Hour * 24 * 30),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString(jm.secret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func (jm *JwtManager) CreateAccessToken(userId uuid.UUID) string {
|
||||
token := jm.createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Access,
|
||||
Expiry: time.Now().Add(time.Minute),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString(jm.secret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
var NotValidToken = errors.New("Not a valid token")
|
||||
|
||||
func (jm *JwtManager) GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
|
||||
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
|
||||
return jm.secret, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Check if token is valid (JWT library validates exp claim automatically)
|
||||
if !token.Valid {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenType, ok := claims["Type"]
|
||||
if !ok || tokenType.(string) != "access" {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(claims["UserID"].(string))
|
||||
if err != nil {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
} else {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
}
|
||||
|
||||
func (jm *JwtManager) GetUserIdFromRefresh(refreshToken string) (uuid.UUID, error) {
|
||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (any, error) {
|
||||
return jm.secret, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Check if token is valid (JWT library validates exp claim automatically)
|
||||
if !token.Valid {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenType, ok := claims["Type"]
|
||||
if !ok || tokenType.(string) != "refresh" {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(claims["UserID"].(string))
|
||||
if err != nil {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
} else {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserIdFromAccess(jm *JwtManager, accessToken string) (uuid.UUID, error) {
|
||||
return jm.GetUserIdFromAccess(accessToken)
|
||||
}
|
||||
36
backend/middleware/limits.go
Normal file
36
backend/middleware/limits.go
Normal file
@ -0,0 +1,36 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func WithLimit(logger *log.Logger, getLimit func(userID uuid.UUID) (bool, error), next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := GetUserID(ctx, logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
hasReachedLimit, err := getLimit(userID)
|
||||
if err != nil {
|
||||
logger.Error("failed to image limit", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Limits", "hasReachedLimit", hasReachedLimit)
|
||||
|
||||
if hasReachedLimit {
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
140
backend/middleware/middleware.go
Normal file
140
backend/middleware/middleware.go
Normal file
@ -0,0 +1,140 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func CorsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Headers", "*")
|
||||
|
||||
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
|
||||
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
|
||||
// The client makes an OPTIONS preflight request before a complex request.
|
||||
// We must handle this and respond with the appropriate headers.
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
const USER_ID = "UserID"
|
||||
|
||||
func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
|
||||
userId := ctx.Value(USER_ID)
|
||||
|
||||
if userId == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not present in request")
|
||||
return uuid.Nil, errors.New("context does not contain a user id")
|
||||
}
|
||||
|
||||
userIdUuid, ok := userId.(uuid.UUID)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not of correct type")
|
||||
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
|
||||
}
|
||||
|
||||
return userIdUuid, nil
|
||||
}
|
||||
|
||||
func ProtectedRouteURL(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ProtectedRoute(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
|
||||
if len(token) < len("Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token[len("Bearer "):])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserIdFromUrl(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
if len(token) == 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||
pathParam := r.PathValue(param)
|
||||
if len(pathParam) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("%s was not present", param)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
uuidParam, err := uuid.Parse(pathParam)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("could not parse param: %w", err)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
return uuidParam, nil
|
||||
}
|
||||
49
backend/middleware/util.go
Normal file
49
backend/middleware/util.go
Normal file
@ -0,0 +1,49 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func WriteJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
|
||||
jsonObject, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type ErrorObject struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func writeError(logger *log.Logger, error string, w http.ResponseWriter, code int) {
|
||||
e := ErrorObject{
|
||||
error,
|
||||
}
|
||||
|
||||
jsonObject, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error("writing error", "error", error)
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func WriteErrorBadRequest(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func WriteErrorInternal(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusInternalServerError)
|
||||
}
|
||||
19
backend/models/database.go
Normal file
19
backend/models/database.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func InitDatabase() (*sql.DB, error) {
|
||||
connection := os.Getenv("DB_CONNECTION")
|
||||
|
||||
if len(connection) == 0 {
|
||||
return nil, errors.New("DB_CONNECTION env was not found.")
|
||||
}
|
||||
|
||||
return sql.Open("postgres", connection)
|
||||
}
|
||||
90
backend/models/image.go
Normal file
90
backend/models/image.go
Normal file
@ -0,0 +1,90 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID uuid.UUID) (model.Image, error) {
|
||||
saveImageStmt := Image.INSERT(Image.ImageName, Image.Image, Image.Description, Image.UserID).
|
||||
VALUES(name, image, "", userID).
|
||||
RETURNING(Image.AllColumns)
|
||||
|
||||
newImage := model.Image{}
|
||||
err := saveImageStmt.QueryContext(ctx, m.dbPool, &newImage)
|
||||
|
||||
return newImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
|
||||
getImageStmt := Image.SELECT(Image.AllColumns).WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
image := model.Image{}
|
||||
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
|
||||
|
||||
return image, err != qrm.ErrNoRows, err
|
||||
}
|
||||
|
||||
func (m ImageModel) UpdateDescription(ctx context.Context, imageID uuid.UUID, description string) error {
|
||||
updateImageDescriptionStmt := Image.UPDATE(Image.Description).
|
||||
SET(Image.Description.SET(String(description))).
|
||||
WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) UpdateProcess(ctx context.Context, imageID uuid.UUID, process model.Progress) error {
|
||||
updateImageDescriptionStmt := Image.UPDATE(Image.Status).
|
||||
SET(process).
|
||||
WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) Update(ctx context.Context, image model.Image) (model.Image, error) {
|
||||
updateImageStmt := Image.UPDATE(Image.MutableColumns.Except(Image.Image)).
|
||||
MODEL(image).
|
||||
WHERE(Image.ID.EQ(UUID(image.ID))).
|
||||
RETURNING(Image.AllColumns.Except(Image.Image))
|
||||
|
||||
updatedImage := model.Image{}
|
||||
err := updateImageStmt.QueryContext(ctx, m.dbPool, &updatedImage)
|
||||
|
||||
return updatedImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) Delete(ctx context.Context, imageID, userID uuid.UUID) (bool, error) {
|
||||
deleteImageStmt := Image.DELETE().WHERE(Image.ID.EQ(UUID(imageID)).AND(Image.UserID.EQ(UUID(userID))))
|
||||
|
||||
r, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("deleting image: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := r.RowsAffected()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unreachable: %w", err)
|
||||
}
|
||||
|
||||
return rowsAffected > 0, nil
|
||||
}
|
||||
|
||||
func NewImageModel(db *sql.DB) ImageModel {
|
||||
return ImageModel{dbPool: db}
|
||||
}
|
||||
200
backend/models/stacks.go
Normal file
200
backend/models/stacks.go
Normal file
@ -0,0 +1,200 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type StackModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type StackWithItems struct {
|
||||
model.Stacks
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
|
||||
type ImageWithSchema struct {
|
||||
model.ImageStacks
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
|
||||
type IDValue struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for lists
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) List(ctx context.Context, userId uuid.UUID) ([]StackWithItems, error) {
|
||||
getStacksWithItems := SELECT(
|
||||
Stacks.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Stacks.
|
||||
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)),
|
||||
).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []StackWithItems{}
|
||||
err := getStacksWithItems.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func (m StackModel) ListItems(ctx context.Context, stackID uuid.UUID) ([]ImageWithSchema, error) {
|
||||
getListItems := SELECT(
|
||||
ImageStacks.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
ImageStacks.
|
||||
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ImageID)),
|
||||
).
|
||||
WHERE(ImageStacks.StackID.EQ(UUID(stackID)))
|
||||
|
||||
listItems := make([]ImageWithSchema, 0)
|
||||
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
|
||||
|
||||
return listItems, err
|
||||
}
|
||||
|
||||
func (m StackModel) Get(ctx context.Context, stackID uuid.UUID) (model.Stacks, error) {
|
||||
getStackStmt := Stacks.SELECT(Stacks.AllColumns).WHERE(Stacks.ID.EQ(UUID(stackID)))
|
||||
|
||||
stack := model.Stacks{}
|
||||
err := getStackStmt.QueryContext(ctx, m.dbPool, &stack)
|
||||
|
||||
return stack, err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INSERT methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) Save(ctx context.Context, userID uuid.UUID, name string, description string, status model.Progress) (model.Stacks, error) {
|
||||
saveListStmt := Stacks.
|
||||
INSERT(Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status).
|
||||
VALUES(userID, name, description, status).
|
||||
RETURNING(Stacks.ID, Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status, Stacks.CreatedAt)
|
||||
|
||||
list := model.Stacks{}
|
||||
err := saveListStmt.QueryContext(ctx, m.dbPool, &list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveItems(ctx context.Context, items []model.SchemaItems) error {
|
||||
saveItemsStmt := SchemaItems.INSERT(SchemaItems.MutableColumns).MODELS(items)
|
||||
|
||||
_, err := saveItemsStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveImage(ctx context.Context, imageID uuid.UUID, stackID uuid.UUID) (model.ImageStacks, error) {
|
||||
saveImageStmt := ImageStacks.
|
||||
INSERT(ImageStacks.ImageID, ImageStacks.StackID).
|
||||
VALUES(imageID, stackID).
|
||||
RETURNING(ImageStacks.AllColumns)
|
||||
|
||||
imageStack := model.ImageStacks{}
|
||||
|
||||
err := saveImageStmt.QueryContext(ctx, m.dbPool, &imageStack)
|
||||
|
||||
return imageStack, err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveSchemaItems(ctx context.Context, imageID uuid.UUID, items []IDValue) error {
|
||||
if len(items) == 0 {
|
||||
return fmt.Errorf("items cannot be empty")
|
||||
}
|
||||
|
||||
saveSchemaItemStmt := ImageSchemaItems.
|
||||
INSERT(
|
||||
ImageSchemaItems.ImageID,
|
||||
ImageSchemaItems.SchemaItemID,
|
||||
ImageSchemaItems.Value,
|
||||
)
|
||||
|
||||
for _, item := range items {
|
||||
saveSchemaItemStmt = saveSchemaItemStmt.VALUES(
|
||||
imageID,
|
||||
item.ID,
|
||||
item.Value,
|
||||
)
|
||||
}
|
||||
|
||||
_, err := saveSchemaItemStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UPDATE methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) UpdateProcess(ctx context.Context, stackID uuid.UUID, process model.Progress) error {
|
||||
updateStackProgressStmt := Stacks.UPDATE(Stacks.Status).
|
||||
SET(process).
|
||||
WHERE(Stacks.ID.EQ(UUID(stackID)))
|
||||
|
||||
_, err := updateStackProgressStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) DeleteSchemaItem(ctx context.Context, stackID uuid.UUID, schemaItemID uuid.UUID) error {
|
||||
deleteImageListStmt := SchemaItems.DELETE().
|
||||
WHERE(
|
||||
SchemaItems.ID.EQ(UUID(schemaItemID)).
|
||||
// The StackID check is a sanity check.
|
||||
// We don't technically need it, but it adds extra protection
|
||||
// in case we make a mistake later on
|
||||
AND(SchemaItems.StackID.EQ(UUID(stackID))),
|
||||
)
|
||||
|
||||
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) DeleteImage(ctx context.Context, stackID uuid.UUID, imageID uuid.UUID) error {
|
||||
deleteImageListStmt := ImageStacks.DELETE().
|
||||
WHERE(
|
||||
ImageStacks.StackID.EQ(UUID(stackID)).
|
||||
AND(ImageStacks.ImageID.EQ(UUID(imageID))),
|
||||
)
|
||||
|
||||
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) Delete(ctx context.Context, stackID uuid.UUID, userID uuid.UUID) error {
|
||||
deleteStackStmt := Stacks.DELETE().WHERE(Stacks.ID.EQ(UUID(stackID)).AND(Stacks.UserID.EQ(UUID(userID))))
|
||||
|
||||
_, err := deleteStackStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewStackModel(db *sql.DB) StackModel {
|
||||
return StackModel{dbPool: db}
|
||||
}
|
||||
108
backend/models/user.go
Normal file
108
backend/models/user.go
Normal file
@ -0,0 +1,108 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type ImageWithProperties struct {
|
||||
ID uuid.UUID
|
||||
|
||||
Image model.Image
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.UUID, error) {
|
||||
getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))
|
||||
|
||||
user := model.Users{}
|
||||
err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)
|
||||
|
||||
return user.ID, err
|
||||
}
|
||||
|
||||
func (m UserModel) DoesUserExist(ctx context.Context, email string) bool {
|
||||
getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))
|
||||
|
||||
user := model.Users{}
|
||||
err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)
|
||||
|
||||
return err != qrm.ErrNoRows
|
||||
}
|
||||
|
||||
func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, error) {
|
||||
insertUserStmt := Users.INSERT(Users.Email).VALUES(user.Email).RETURNING(Users.AllColumns)
|
||||
|
||||
insertedUser := model.Users{}
|
||||
err := insertUserStmt.QueryContext(ctx, m.dbPool, &insertedUser)
|
||||
|
||||
return insertedUser, err
|
||||
}
|
||||
|
||||
type UserImageWithImage struct {
|
||||
model.Image
|
||||
ImageStacks []model.ImageStacks
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
|
||||
getUserImagesStmt := SELECT(
|
||||
Image.AllColumns.Except(Image.Image),
|
||||
ImageStacks.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Image.
|
||||
LEFT_JOIN(ImageStacks, ImageStacks.ImageID.EQ(Image.ID)),
|
||||
).
|
||||
WHERE(Image.UserID.EQ(UUID(userId)))
|
||||
|
||||
userImages := []UserImageWithImage{}
|
||||
err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)
|
||||
|
||||
return userImages, err
|
||||
}
|
||||
|
||||
type ListsWithImages struct {
|
||||
model.Stacks
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
|
||||
Images []struct {
|
||||
model.ImageStacks
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
|
||||
stmt := SELECT(
|
||||
Stacks.AllColumns,
|
||||
ImageStacks.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Stacks.
|
||||
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)).
|
||||
LEFT_JOIN(ImageStacks, ImageStacks.StackID.EQ(Stacks.ID)).
|
||||
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ID)),
|
||||
).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []ListsWithImages{}
|
||||
err := stmt.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func NewUserModel(db *sql.DB) UserModel {
|
||||
return UserModel{dbPool: db}
|
||||
}
|
||||
38
backend/notifications/channel_splitter.go
Normal file
38
backend/notifications/channel_splitter.go
Normal file
@ -0,0 +1,38 @@
|
||||
package notifications
|
||||
|
||||
type ChannelSplitter[TNotification any] struct {
|
||||
ch chan TNotification
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Listen() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.ch:
|
||||
for _, v := range s.Listeners {
|
||||
v <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Add(id string) chan TNotification {
|
||||
ch := make(chan TNotification)
|
||||
s.Listeners[id] = ch
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Remove(id string) {
|
||||
delete(s.Listeners, id)
|
||||
}
|
||||
|
||||
func NewChannelSplitter[TNotification any](ch chan TNotification) ChannelSplitter[TNotification] {
|
||||
return ChannelSplitter[TNotification]{
|
||||
ch: ch,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
||||
64
backend/notifications/entities_notification.go
Normal file
64
backend/notifications/entities_notification.go
Normal file
@ -0,0 +1,64 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
IMAGE_TYPE = "image"
|
||||
STACK_TYPE = "stack"
|
||||
)
|
||||
|
||||
type ImageNotification struct {
|
||||
Type string
|
||||
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type StackNotification struct {
|
||||
Type string
|
||||
|
||||
StackID uuid.UUID
|
||||
Name string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
image *ImageNotification
|
||||
stack *StackNotification
|
||||
}
|
||||
|
||||
func GetImageNotification(image ImageNotification) Notification {
|
||||
return Notification{
|
||||
image: &image,
|
||||
}
|
||||
}
|
||||
|
||||
func GetStackNotification(list StackNotification) Notification {
|
||||
return Notification{
|
||||
stack: &list,
|
||||
}
|
||||
}
|
||||
|
||||
func (n Notification) MarshalJSON() ([]byte, error) {
|
||||
if n.image != nil {
|
||||
return json.Marshal(n.image)
|
||||
}
|
||||
|
||||
if n.stack != nil {
|
||||
return json.Marshal(n.stack)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no image or list present")
|
||||
}
|
||||
|
||||
func (n *Notification) UnmarshalJSON(data []byte) error {
|
||||
return fmt.Errorf("unimplemented")
|
||||
}
|
||||
58
backend/notifications/notifications.go
Normal file
58
backend/notifications/notifications.go
Normal file
@ -0,0 +1,58 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Notifier[TNotification any] struct {
|
||||
bufferSize int
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (n *Notifier[TNotification]) Create(id string) error {
|
||||
if _, exists := n.Listeners[id]; exists {
|
||||
return errors.New("This listener already exists")
|
||||
}
|
||||
|
||||
n.Listeners[id] = make(chan TNotification, n.bufferSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ChannelFullErr = errors.New("Channel is full")
|
||||
|
||||
// Ensures the listener exists before sending
|
||||
func (n *Notifier[TNotification]) SendAndCreate(id string, notification TNotification) error {
|
||||
if _, exists := n.Listeners[id]; !exists {
|
||||
if err := n.Create(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ch := n.Listeners[id]
|
||||
|
||||
select {
|
||||
case ch <- notification:
|
||||
return nil
|
||||
default:
|
||||
return ChannelFullErr
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier[TNotification]) Delete(id string) error {
|
||||
if _, exists := n.Listeners[id]; !exists {
|
||||
return errors.New("This listener does not exists")
|
||||
}
|
||||
|
||||
delete(n.Listeners, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewNotifier[TNotification any](bufferSize int) Notifier[TNotification] {
|
||||
return Notifier[TNotification]{
|
||||
bufferSize: bufferSize,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
||||
48
backend/notifications/notifications_test.go
Normal file
48
backend/notifications/notifications_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSendingNotifications(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
notifier := NewNotifier[string](3)
|
||||
|
||||
err := notifier.SendAndCreate("1", "a")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "b")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "c")
|
||||
require.NoError(err)
|
||||
|
||||
ch := notifier.Listeners["1"]
|
||||
|
||||
a := <-ch
|
||||
b := <-ch
|
||||
c := <-ch
|
||||
|
||||
assert.Equal(a, "a")
|
||||
assert.Equal(b, "b")
|
||||
assert.Equal(c, "c")
|
||||
}
|
||||
|
||||
func TestFullBuffer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
notifier := NewNotifier[string](1)
|
||||
|
||||
err := notifier.SendAndCreate("1", "a")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "b")
|
||||
|
||||
assert.Error(err)
|
||||
}
|
||||
157
backend/processor/image.go
Normal file
157
backend/processor/image.go
Normal file
@ -0,0 +1,157 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const IMAGE_PROCESS_AT_A_TIME = 10
|
||||
|
||||
type ImageProcessor struct {
|
||||
imageModel models.ImageModel
|
||||
logger *log.Logger
|
||||
|
||||
descriptionAgent agents.DescriptionAgent
|
||||
stackAgent client.AgentClient
|
||||
|
||||
Processor *Processor[model.Image]
|
||||
notifier *notifications.Notifier[notifications.Notification]
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
|
||||
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_InProgress)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update image", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) setImageToDone(ctx context.Context, image model.Image) {
|
||||
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_Complete)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update image", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) describe(ctx context.Context, image model.Image) {
|
||||
descriptionSubLogger := p.logger.With("describe image", image.ID)
|
||||
|
||||
err := p.descriptionAgent.Describe(descriptionSubLogger, image.ID, image.ImageName, image.Image)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to describe image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) extractInfo(ctx context.Context, image model.Image) {
|
||||
err := p.stackAgent.RunAgent(image.UserID, image.ID, image.ImageName, image.Image)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to process image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) processImage(image model.Image) {
|
||||
p.logger.Info("Processing image", "ID", image.ID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
p.setImageToProcess(ctx, image)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
imageNotification := notifications.GetImageNotification(notifications.ImageNotification{
|
||||
Type: notifications.IMAGE_TYPE,
|
||||
ImageID: image.ID,
|
||||
ImageName: image.ImageName,
|
||||
Status: string(model.Progress_InProgress),
|
||||
})
|
||||
|
||||
err := p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending in progress notification", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
p.describe(ctx, image)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
p.extractInfo(ctx, image)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
p.setImageToDone(ctx, image)
|
||||
|
||||
// TODO: there is some repeated code here. The ergonomicts of the notifications,
|
||||
// isn't the best.
|
||||
imageNotification = notifications.GetImageNotification(notifications.ImageNotification{
|
||||
Type: notifications.IMAGE_TYPE,
|
||||
ImageID: image.ID,
|
||||
ImageName: image.ImageName,
|
||||
Status: string(model.Progress_Complete),
|
||||
})
|
||||
|
||||
err = p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending done notification", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewImageProcessor(
|
||||
logger *log.Logger,
|
||||
imageModel models.ImageModel,
|
||||
listModel models.StackModel,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
notifier *notifications.Notifier[notifications.Notification],
|
||||
) (ImageProcessor, error) {
|
||||
if notifier == nil {
|
||||
return ImageProcessor{}, fmt.Errorf("notifier is nil")
|
||||
}
|
||||
|
||||
descriptionAgent := agents.NewDescriptionAgent(logger, imageModel)
|
||||
stackAgent := agents.NewStackAgent(logger, listModel, limitsManager)
|
||||
|
||||
imageProcessor := ImageProcessor{
|
||||
imageModel: imageModel,
|
||||
logger: logger,
|
||||
descriptionAgent: descriptionAgent,
|
||||
stackAgent: stackAgent,
|
||||
|
||||
notifier: notifier,
|
||||
}
|
||||
|
||||
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
|
||||
|
||||
return imageProcessor, nil
|
||||
}
|
||||
23
backend/processor/processor.go
Normal file
23
backend/processor/processor.go
Normal file
@ -0,0 +1,23 @@
|
||||
package processor
|
||||
|
||||
type Processor[TMessage any] struct {
|
||||
queue chan TMessage
|
||||
process func(message TMessage)
|
||||
}
|
||||
|
||||
func (p *Processor[TMessage]) Work() {
|
||||
for msg := range p.queue {
|
||||
p.process(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor[TMessage]) Add(msg TMessage) {
|
||||
p.queue <- msg
|
||||
}
|
||||
|
||||
func NewProcessor[TMessage any](bufferSize int, process func(message TMessage)) *Processor[TMessage] {
|
||||
return &Processor[TMessage]{
|
||||
queue: make(chan TMessage, bufferSize),
|
||||
process: process,
|
||||
}
|
||||
}
|
||||
142
backend/processor/stack.go
Normal file
142
backend/processor/stack.go
Normal file
@ -0,0 +1,142 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const STACK_PROCESS_AT_A_TIME = 10
|
||||
|
||||
// TODO:
|
||||
// This processor contains a lot of shared stuff.
|
||||
// If we ever want to do more generic stuff with "in-progress" and stuff
|
||||
// we can extract that into a common thing
|
||||
//
|
||||
// However, this will require a pretty big DB shuffle.
|
||||
|
||||
type StackProcessor struct {
|
||||
stackModel models.StackModel
|
||||
logger *log.Logger
|
||||
|
||||
stackAgent agents.CreateListAgent
|
||||
|
||||
Processor *Processor[model.Stacks]
|
||||
|
||||
notifier *notifications.Notifier[notifications.Notification]
|
||||
}
|
||||
|
||||
func (p *StackProcessor) setStackToProcess(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_InProgress)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update stack", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) setStackToDone(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_Complete)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update stack", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) extractInfo(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackAgent.CreateList(p.logger, stack.UserID, stack.ID, stack.Name, stack.Description)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to process image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) processImage(stack model.Stacks) {
|
||||
p.logger.Info("Processing image", "ID", stack.ID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
p.setStackToProcess(ctx, stack)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Future proofing!
|
||||
wg.Add(1)
|
||||
|
||||
stackNotification := notifications.GetStackNotification(notifications.StackNotification{
|
||||
Type: notifications.STACK_TYPE,
|
||||
Status: string(model.Progress_InProgress),
|
||||
StackID: stack.ID,
|
||||
Name: stack.Name,
|
||||
})
|
||||
|
||||
err := p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending in progress notification", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
p.extractInfo(ctx, stack)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
p.setStackToDone(ctx, stack)
|
||||
|
||||
// TODO: there is some repeated code here. The ergonomicts of the notifications,
|
||||
// isn't the best.
|
||||
stackNotification = notifications.GetStackNotification(notifications.StackNotification{
|
||||
Type: notifications.STACK_TYPE,
|
||||
Status: string(model.Progress_Complete),
|
||||
StackID: stack.ID,
|
||||
Name: stack.Name,
|
||||
})
|
||||
|
||||
err = p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending done notification", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewStackProcessor(
|
||||
logger *log.Logger,
|
||||
stackModel models.StackModel,
|
||||
notifier *notifications.Notifier[notifications.Notification],
|
||||
) (StackProcessor, error) {
|
||||
if notifier == nil {
|
||||
return StackProcessor{}, fmt.Errorf("notifier is nil")
|
||||
}
|
||||
|
||||
stackAgent := agents.NewCreateListAgent(logger, stackModel)
|
||||
|
||||
imageProcessor := StackProcessor{
|
||||
logger: logger,
|
||||
stackModel: stackModel,
|
||||
stackAgent: stackAgent,
|
||||
notifier: notifier,
|
||||
}
|
||||
|
||||
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
|
||||
|
||||
return imageProcessor, nil
|
||||
}
|
||||
73
backend/router.go
Normal file
73
backend/router.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"screenmark/screenmark/processor"
|
||||
"screenmark/screenmark/stacks"
|
||||
|
||||
ourmiddleware "screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) (chi.Router, error) {
|
||||
limitsManager := limits.CreateLimitsManager(db)
|
||||
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewStackModel(db)
|
||||
|
||||
notifier := notifications.NewNotifier[notifications.Notification](10)
|
||||
|
||||
imageProcessorLogger := createLogger("Image Processor", os.Stdout)
|
||||
imageProcessor, err := processor.NewImageProcessor(imageProcessorLogger, imageModel, stackModel, limitsManager, ¬ifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processor: %w", err)
|
||||
}
|
||||
|
||||
stackProcessorLog := createLogger("Stack Processor", os.Stdout)
|
||||
stackProcessor, err := processor.NewStackProcessor(stackProcessorLog, stackModel, ¬ifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processor: %w", err)
|
||||
}
|
||||
|
||||
go imageProcessor.Processor.Work()
|
||||
go stackProcessor.Processor.Work()
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager, stackProcessor.Processor)
|
||||
authHandler := auth.CreateAuthHandler(db, jwtManager)
|
||||
imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(ourmiddleware.CorsMiddleware)
|
||||
|
||||
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||
r.Route("/auth", authHandler.CreateRoutes)
|
||||
r.Route("/images", imageHandler.CreateRoutes)
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(ourmiddleware.GetUserIdFromUrl(jwtManager))
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
return r, nil
|
||||
}
|
||||
70
backend/schema.sql
Normal file
70
backend/schema.sql
Normal file
@ -0,0 +1,70 @@
|
||||
DROP SCHEMA IF EXISTS haystack CASCADE;
|
||||
|
||||
CREATE SCHEMA haystack;
|
||||
|
||||
/* -----| Enums |----- */
|
||||
|
||||
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
|
||||
|
||||
/* -----| Schema tables |----- */
|
||||
|
||||
CREATE TABLE haystack.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
image_name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
|
||||
image BYTEA NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.stacks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_stacks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
|
||||
stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE,
|
||||
|
||||
UNIQUE(image_id, stack_id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
item TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
value TEXT,
|
||||
|
||||
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id) ON DELETE CASCADE,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image_stacks (id) ON DELETE CASCADE
|
||||
);
|
||||
263
backend/stacks/handler.go
Normal file
263
backend/stacks/handler.go
Normal file
@ -0,0 +1,263 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/processor"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type StackHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
imageModel models.ImageModel
|
||||
stackModel models.StackModel
|
||||
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
|
||||
processor *processor.Processor[model.Stacks]
|
||||
}
|
||||
|
||||
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stacks, err := h.stackModel.List(ctx, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get stacks", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, stacks, w)
|
||||
}
|
||||
|
||||
func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
_, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: must check for permission here.
|
||||
|
||||
lists, err := h.stackModel.ListItems(ctx, stackID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get list items", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||
}
|
||||
|
||||
type EditStack struct {
|
||||
Hello string `json:"hello"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.Delete(ctx, stackID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not delete stack", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stringListID := chi.URLParam(r, "stackID")
|
||||
stringImageID := chi.URLParam(r, "imageID")
|
||||
|
||||
imageID, err := uuid.Parse(stringImageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := uuid.Parse(stringListID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this should be extracted into a middleware of sorts
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := h.stackModel.Get(ctx, stackID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if stack.UserID != userID {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.DeleteImage(ctx, stackID, imageID)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to delete image from list", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stringStackID := chi.URLParam(r, "stackID")
|
||||
stringSchemaItemID := chi.URLParam(r, "schemaItemID")
|
||||
|
||||
stackID, err := uuid.Parse(stringStackID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
schemaItemID, err := uuid.Parse(stringSchemaItemID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this should be extracted into a middleware of sorts
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := h.stackModel.Get(ctx, stackID)
|
||||
if err != nil {
|
||||
h.logger.Error("could not get stack model", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if stack.UserID != userID {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// The code above is repeated, because it contains stack & image
|
||||
// manipulations. So we could create a middleware.
|
||||
// If you repeat this 3 times, then organise it :)
|
||||
|
||||
err = h.stackModel.DeleteSchemaItem(ctx, stackID, schemaItemID)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to delete image from list", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreateStackBody struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add the stack processor here
|
||||
stack, err := h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not save stack", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.processor.Add(stack)
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, stack, w)
|
||||
}
|
||||
|
||||
func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting stack router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute(h.jwtManager))
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.getAllStacks)
|
||||
r.Get("/{stackID}", h.getStackItems)
|
||||
|
||||
r.Post("/", middleware.WithLimit(h.logger, h.limitsManager.HasReachedStackLimit, middleware.WithValidatedPost(h.createStack)))
|
||||
r.Patch("/{stackID}", middleware.WithValidatedPost(h.editStack))
|
||||
r.Delete("/{stackID}", h.deleteStack)
|
||||
r.Delete("/{stackID}/{imageID}", h.deleteImageFromStack)
|
||||
r.Delete("/{stackID}/{schemaItemID}", h.deleteImageStackSchemaItem)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateStackHandler(
|
||||
db *sql.DB,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
jwtManager *middleware.JwtManager,
|
||||
processor *processor.Processor[model.Stacks],
|
||||
) StackHandler {
|
||||
stackModel := models.NewStackModel(db)
|
||||
imageModel := models.NewImageModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Stacks")
|
||||
|
||||
return StackHandler{
|
||||
logger: logger,
|
||||
imageModel: imageModel,
|
||||
stackModel: stackModel,
|
||||
limitsManager: limitsManager,
|
||||
jwtManager: jwtManager,
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
4
backup.bash
Normal file
4
backup.bash
Normal file
@ -0,0 +1,4 @@
|
||||
name=haystack-db-dump-$(date "+%Y-%m-%d").sql
|
||||
pg_dump haystack > $name
|
||||
rsync -avH $name zh3586@zh3586.rsync.net:Backups/Haystack/
|
||||
rm $name
|
||||
17
biome.json
Normal file
17
biome.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4
|
||||
}
|
||||
}
|
||||
BIN
frontend/.DS_Store
vendored
Normal file
BIN
frontend/.DS_Store
vendored
Normal file
Binary file not shown.
7
frontend/.gitignore
vendored
Normal file
7
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.env
|
||||
db
|
||||
screenmark
|
||||
node_modules
|
||||
dist
|
||||
tsconfig.node.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
3
frontend/.idea/.gitignore
generated
vendored
Normal file
3
frontend/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
607
frontend/.idea/caches/deviceStreaming.xml
generated
Normal file
607
frontend/.idea/caches/deviceStreaming.xml
generated
Normal file
@ -0,0 +1,607 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceStreaming">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP5552L1" />
|
||||
<option name="id" value="OP5552L1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2415" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OPPO" />
|
||||
<option name="codename" value="OP573DL1" />
|
||||
<option name="id" value="OP573DL1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OPPO" />
|
||||
<option name="name" value="CPH2557" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a15" />
|
||||
<option name="id" value="a15" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A15" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a35x" />
|
||||
<option name="id" value="a35x" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A35" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="arcfox" />
|
||||
<option name="id" value="arcfox" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="razr plus 2024" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="1272" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="austin" />
|
||||
<option name="id" value="austin" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g 5G (2022)" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm2q" />
|
||||
<option name="id" value="dm2q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="S23 Plus" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e3q" />
|
||||
<option name="id" value="e3q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24 Ultra" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3120" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="eos" />
|
||||
<option name="id" value="eos" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Eos" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="fogona" />
|
||||
<option name="id" value="fogona" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g play - 2024" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="g0q" />
|
||||
<option name="id" value="g0q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-S906U1" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gta9pwifi" />
|
||||
<option name="id" value="gta9pwifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-X210" />
|
||||
<option name="screenDensity" value="240" />
|
||||
<option name="screenX" value="1200" />
|
||||
<option name="screenY" value="1920" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts7xllite" />
|
||||
<option name="id" value="gts7xllite" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-T738U" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8wifi" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="gts8wifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8" />
|
||||
<option name="screenDensity" value="274" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts9fe" />
|
||||
<option name="id" value="gts9fe" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="2304" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="maui" />
|
||||
<option name="id" value="maui" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g play - 2023" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="o1q" />
|
||||
<option name="id" value="o1q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21" />
|
||||
<option name="screenDensity" value="421" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="formFactor" value="Wear OS" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="r11q" />
|
||||
<option name="id" value="r11q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-S711U" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="t2q" />
|
||||
<option name="id" value="t2q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21 Plus" />
|
||||
<option name="screenDensity" value="394" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
9
frontend/.idea/frontend.iml
generated
Normal file
9
frontend/.idea/frontend.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
frontend/.idea/misc.xml
generated
Normal file
6
frontend/.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
frontend/.idea/modules.xml
generated
Normal file
8
frontend/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
frontend/.idea/vcs.xml
generated
Normal file
6
frontend/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
53
frontend/.idea/workspace.xml
generated
Normal file
53
frontend/.idea/workspace.xml
generated
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="NONE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="4ea94c05-c21c-40f9-ad16-43233a3011ee" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ClangdSettings">
|
||||
<option name="formatViaClangd" value="false" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 5
|
||||
}</component>
|
||||
<component name="ProjectId" id="2w23zazSC8gW9XDwUxbl8Fam8DV" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||
"cf.first.check.clang-format": "false",
|
||||
"cidr.known.project.marker": "true",
|
||||
"com.google.services.firebase.aqiPopupShown": "true",
|
||||
"git-widget-placeholder": "feat/android-version",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "/home/johnc/Code/haystack-app/frontend",
|
||||
"settings.editor.selected.configurable": "AndroidSdkUpdater"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="4ea94c05-c21c-40f9-ad16-43233a3011ee" name="Changes" comment="" />
|
||||
<created>1745226104717</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1745226104717</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
</project>
|
||||
951
frontend/bun.lock
Normal file
951
frontend/bun.lock
Normal file
@ -0,0 +1,951 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "haystack",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "^0.13.11",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tabler/icons-solidjs": "^3.35.0",
|
||||
"@tanstack/solid-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"@tauri-apps/plugin-dialog": "~2.3.3",
|
||||
"@tauri-apps/plugin-fs": "~2.4.2",
|
||||
"@tauri-apps/plugin-http": "^2.4.3",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"solid-js": "^1.9.9",
|
||||
"solid-markdown": "^2.0.14",
|
||||
"solid-motionone": "^1.0.4",
|
||||
"solidjs-markdown": "^0.2.0",
|
||||
"tailwind-scrollbar-hide": "^2.0.0",
|
||||
"tauri-plugin-ios-shared-token-api": "file:../tauri-plugin-ios-shared-token",
|
||||
"tauri-plugin-sharetarget-api": "^0.1.6",
|
||||
"valibot": "^1.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@tauri-apps/cli": "^2.8.4",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"tailwindcss": "3.4.0",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-solid": "^2.11.8",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
||||
|
||||
"@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.8.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw=="],
|
||||
|
||||
"@internationalized/number": ["@internationalized/number@3.6.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-UVsb4bCwbL944E0SX50CHFtWEeZ2uB5VozZ5yDXJdq6iPZsZO5p+bjVMZh2GxHf4Bs/7xtDCcPwEa2NU9DaG/g=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="],
|
||||
|
||||
"@kobalte/tailwindcss": ["@kobalte/tailwindcss@0.9.0", "", { "peerDependencies": { "tailwindcss": "^3.3.3" } }, "sha512-WbueJTVRiO4yrmfHIBwp07y3M5iibJ/gauEAQ7mOyg1tZulvpO7SM/UdgzX95a9a0KDt1mQFxwO7RmpOUXWOWA=="],
|
||||
|
||||
"@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
|
||||
|
||||
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
||||
|
||||
"@motionone/dom": ["@motionone/dom@10.18.0", "", { "dependencies": { "@motionone/animation": "^10.18.0", "@motionone/generators": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A=="],
|
||||
|
||||
"@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="],
|
||||
|
||||
"@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="],
|
||||
|
||||
"@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="],
|
||||
|
||||
"@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@rollup/plugin-typescript": ["@rollup/plugin-typescript@11.1.6", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||
|
||||
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-g04CXywgWG/7L4sTxQP6q1gdiirItVBq6ZO9YuLTqPFlkX3uD4IEjeL9cLHP6waahrnO8yL3OZl64pcKGYN5Qw=="],
|
||||
|
||||
"@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="],
|
||||
|
||||
"@solid-primitives/media": ["@solid-primitives/media@2.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7+C3wfbWnGE/WPoNsqcp/EeOP2aNNB92RCpsWhBth8E5lZo/J+rK6jMb7umVsK0zguT8HBpeXp1pFyFbcsHStA=="],
|
||||
|
||||
"@solid-primitives/props": ["@solid-primitives/props@3.2.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-vEg5yERdXftJz2+A6B0IMYTrPL9SE2DPmpURV/nZyqQ+PXziF02V4b4SDr6JX3jNJxBlY6c17LqwYEw+bIfGRg=="],
|
||||
|
||||
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
||||
|
||||
"@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-tO9MDAc2pNjpcRd5B8LWbiR1qzIgvGZ5BtTuO98N7CLwd+fnuyGwtlQtJpz5hcLcTnoawpQYLpiRGNgaYW+YzQ=="],
|
||||
|
||||
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-YJ+EveQeDv9DLqfDKfsPAAGy2x3vBruoD23yn+nD2dT84QjoBxWT1T0qA0TMFjek6/xuN3flqnHtQ4r++4zdjg=="],
|
||||
|
||||
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-6Coau0Kv/dF83UQpbBzc+gnJafOQAPe2jCbB4jmTK5UocsR5cWmFBVRm3kin+nZFVaO4WkuELw0cKANWgTVh8Q=="],
|
||||
|
||||
"@solid-primitives/transition-group": ["@solid-primitives/transition-group@1.1.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-pL1sEPCHuC4V+Yh+SQsKSPuGDYrZbLJYSkk3AB4TZrWhptEJUS0IHoi7BAynYcMiULbvMMVKFbeFHqINZq0+ig=="],
|
||||
|
||||
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sW4/3cDXSjYQampn8CIFZ11BlxgNf2li8r2fXnb3b3YWE6RdZZCl8PhvpPF38Gzl0CnryrbTPJWM7OIkseCDgQ=="],
|
||||
|
||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||
|
||||
"@tabler/icons": ["@tabler/icons@3.35.0", "", {}, "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ=="],
|
||||
|
||||
"@tabler/icons-solidjs": ["@tabler/icons-solidjs@3.35.0", "", { "dependencies": { "@tabler/icons": "3.35.0" }, "peerDependencies": { "solid-js": "^1.4.7" } }, "sha512-9kJxO7ITryM30xgmXJgYkebGXRjXIKIwue5g8AQfk+z0eNLFZqWz5w1833KPSNy/2k/86Pe0IOZJ4Gav3Th5xw=="],
|
||||
|
||||
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-0dS8GkBTmbuM9cUR6Jni0a45eJbd32CAEbZj8HrZMWIj3lu974NpGz5ywcomOGJ9GdeHuDaRzlwtonBbKV1ihQ=="],
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
|
||||
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="],
|
||||
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.8.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.8.4", "@tauri-apps/cli-darwin-x64": "2.8.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4", "@tauri-apps/cli-linux-arm64-gnu": "2.8.4", "@tauri-apps/cli-linux-arm64-musl": "2.8.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-musl": "2.8.4", "@tauri-apps/cli-win32-arm64-msvc": "2.8.4", "@tauri-apps/cli-win32-ia32-msvc": "2.8.4", "@tauri-apps/cli-win32-x64-msvc": "2.8.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.8.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.8.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA=="],
|
||||
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.8.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w=="],
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.8.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA=="],
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.8.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA=="],
|
||||
|
||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-cWXB9QJDbLIA0v7I5QY183awazBEQNPhp19iPvrMZoJRX8SbFkhWFx1/q7zy7xGpXXzxz29qtq6z21Ho7W5Iew=="],
|
||||
|
||||
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig=="],
|
||||
|
||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-x1mQKHSLDk4mS2S938OTeyk8L7QyLpCrKZCZcjkljGsvTvRMojCvI9SeJ1kaxc7t8xSilkC7WdId8xER9TIGLg=="],
|
||||
|
||||
"@tauri-apps/plugin-log": ["@tauri-apps/plugin-log@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-81XQ2f93x4vmIB5OY0XlYAxy60cHdYLs0Ki8Qp38tNATRiuBit+Orh3frpY3qfYQnqEvYVyRub7YRJWlmW2RRA=="],
|
||||
|
||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA=="],
|
||||
|
||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ty5V8XDUIFbSnrk3zsFoP3kzN+vAufYzalJSlmrVhQTImIZa1aL1a03bOaP2vuBvfR+WDRC6NgV2xBl8G07d+w=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.39.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8GzVmFla7jaTNWW8W+lTMl9YGva4/06CtwJjySnkYtt8G1v9weCzc2SuF1DfrudcCNb2Doetc1FRg33swBYZCA=="],
|
||||
|
||||
"babel-preset-solid": ["babel-preset-solid@1.9.5", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.39.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-85I3osODJ1LvZbv8wFozROV1vXq32BubqHXAGu73A//TRs3NLI1OFP83AQBUTSQHwgZQmARjHlJciym3we+V+w=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001715", "", {}, "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
|
||||
|
||||
"dependency-graph": ["dependency-graph@1.0.0", "", {}, "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||
|
||||
"diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.143", "", {}, "sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@1.2.0", "", { "dependencies": { "@types/hast": "^2.0.0", "@types/unist": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^0.4.1", "unist-util-position": "^4.0.0", "vfile-message": "^3.0.0" } }, "sha512-Y4FB8Dx2k6zJZrwbexkVm6YVRA8Sho2tTwacjDSr/x5c0wioOpc1VIoLyGUSb8+8xkAnQPAtHbdMvzA6bl0F1w=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@2.0.1", "", {}, "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng=="],
|
||||
|
||||
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
|
||||
|
||||
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.1.1", "", {}, "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
|
||||
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"mdast-util-definitions": ["mdast-util-definitions@5.1.2", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "unist-util-visit": "^4.0.0" } }, "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-cli": ["postcss-cli@11.0.1", "", { "dependencies": { "chokidar": "^3.3.0", "dependency-graph": "^1.0.0", "fs-extra": "^11.0.0", "picocolors": "^1.0.0", "postcss-load-config": "^5.0.0", "postcss-reporter": "^7.0.0", "pretty-hrtime": "^1.0.3", "read-cache": "^1.0.0", "slash": "^5.0.0", "tinyglobby": "^0.2.12", "yargs": "^17.0.0" }, "peerDependencies": { "postcss": "^8.0.0" }, "bin": { "postcss": "index.js" } }, "sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g=="],
|
||||
|
||||
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||
|
||||
"postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@5.1.0", "", { "dependencies": { "lilconfig": "^3.1.1", "yaml": "^2.4.2" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1" }, "optionalPeers": ["jiti", "postcss", "tsx"] }, "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA=="],
|
||||
|
||||
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||
|
||||
"postcss-reporter": ["postcss-reporter@7.1.0", "", { "dependencies": { "picocolors": "^1.0.0", "thenby": "^1.3.4" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"pretty-hrtime": ["pretty-hrtime@1.0.3", "", {}, "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="],
|
||||
|
||||
"property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.3.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="],
|
||||
|
||||
"solid-jsx": ["solid-jsx@0.9.1", "", { "peerDependencies": { "solid-js": "^1.4.0" } }, "sha512-HHTx58rx3tqg5LMGuQnaE1vqZjpl+RMP0jYQnBkTY0xKIASVNSLZJCZoPFrpKH8wWWYyTLHdepgzs8u/e6yz5Q=="],
|
||||
|
||||
"solid-markdown": ["solid-markdown@2.0.14", "", { "dependencies": { "comma-separated-tokens": "^2.0.3", "property-information": "^6.3.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "space-separated-tokens": "^2.0.2", "style-to-object": "^0.3.0", "unified": "^11.0.5", "unist-util-visit": "^4.1.2", "vfile": "^6.0.3" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-Ln8R4TsNWySXvKkS80OHV+CSR/mwjk5XfGvC5UjZo/y/rAbbkBoxt6FXoWsfCkTW6GH9yxYvahSMXsUJU/ov4Q=="],
|
||||
|
||||
"solid-motionone": ["solid-motionone@1.0.4", "", { "dependencies": { "@motionone/dom": "^10.17.0", "@motionone/utils": "^10.17.0", "@solid-primitives/props": "^3.1.11", "@solid-primitives/refs": "^1.0.8", "@solid-primitives/transition-group": "^1.0.5", "csstype": "^3.1.3" }, "peerDependencies": { "solid-js": "^1.8.0" } }, "sha512-aqEjgecoO9raDFznu/dEci7ORSmA26Kjj9J4Cn1Gyr0GZuOVdvsNxdxClTL9J40Aq/uYFx4GLwC8n70fMLHiuA=="],
|
||||
|
||||
"solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
|
||||
|
||||
"solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="],
|
||||
|
||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||
|
||||
"solidjs-markdown": ["solidjs-markdown@0.2.0", "", { "dependencies": { "hast-util-to-jsx-runtime": "^1.2.0", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "solid-jsx": "^0.9.1", "unified": "^10.1.2", "vfile": "^5.3.7" } }, "sha512-7jiqmM2Z9Z9EFmE7MtI6KJtQboapUyjtu30RvhO00RZWKP04pNxQpDksNnU9ItnMj50/3mNlV0gx2DoqjrxdHA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"style-to-object": ["style-to-object@0.3.0", "", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@2.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@3.4.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA=="],
|
||||
|
||||
"tauri-plugin-ios-shared-token-api": ["tauri-plugin-ios-shared-token-api@file:../tauri-plugin-ios-shared-token", { "dependencies": { "@tauri-apps/api": ">=2.0.0-beta.6" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.6", "rollup": "^4.9.6", "tslib": "^2.6.2", "typescript": "^5.3.3" } }],
|
||||
|
||||
"tauri-plugin-sharetarget-api": ["tauri-plugin-sharetarget-api@0.1.6", "", { "dependencies": { "@tauri-apps/api": ">=2.0.0-beta.6" } }, "sha512-R9LgXu7hn8LaCW5ll8tJ4es0N2nCNVnptT4v3HTXInDra9pm5CuXalX3m5W7+/XTwOFn0sMRv4eOKz8S5FEy1w=="],
|
||||
|
||||
"thenby": ["thenby@1.3.4", "", {}, "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-generated": ["unist-util-generated@2.0.1", "", {}, "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@4.0.4", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@3.0.3", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uvu": ["uvu@0.5.6", "", { "dependencies": { "dequal": "^2.0.0", "diff": "^5.0.0", "kleur": "^4.0.3", "sade": "^1.7.3" }, "bin": { "uvu": "bin.js" } }, "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA=="],
|
||||
|
||||
"valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="],
|
||||
|
||||
"validate-html-nesting": ["validate-html-nesting@1.2.2", "", {}, "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||
|
||||
"vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="],
|
||||
|
||||
"vite-plugin-solid": ["vite-plugin-solid@2.11.8", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
|
||||
|
||||
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"hast-util-to-jsx-runtime/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||
|
||||
"hast-util-to-jsx-runtime/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"hast-util-to-jsx-runtime/style-to-object": ["style-to-object@0.4.4", "", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg=="],
|
||||
|
||||
"hast-util-to-jsx-runtime/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="],
|
||||
|
||||
"mdast-util-definitions/@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="],
|
||||
|
||||
"mdast-util-definitions/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"mdast-util-from-markdown/unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse": ["remark-parse@10.0.2", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-from-markdown": "^1.0.0", "unified": "^10.0.0" } }, "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype": ["remark-rehype@10.1.0", "", { "dependencies": { "@types/hast": "^2.0.0", "@types/mdast": "^3.0.0", "mdast-util-to-hast": "^12.1.0", "unified": "^10.0.0" } }, "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw=="],
|
||||
|
||||
"solidjs-markdown/unified": ["unified@10.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "bail": "^2.0.0", "extend": "^3.0.0", "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^5.0.0" } }, "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q=="],
|
||||
|
||||
"solidjs-markdown/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="],
|
||||
|
||||
"tailwindcss/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
|
||||
|
||||
"tauri-plugin-sharetarget-api/@tauri-apps/api": ["@tauri-apps/api@2.5.0", "", {}, "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA=="],
|
||||
|
||||
"unist-util-is/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"unist-util-position/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"unist-util-stringify-position/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"unist-util-visit-parents/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"vfile-message/unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-visit/unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@1.3.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "decode-named-character-reference": "^1.0.0", "mdast-util-to-string": "^3.1.0", "micromark": "^3.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-decode-string": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "unist-util-stringify-position": "^3.0.0", "uvu": "^0.5.0" } }, "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast": ["mdast-util-to-hast@12.3.0", "", { "dependencies": { "@types/hast": "^2.0.0", "@types/mdast": "^3.0.0", "mdast-util-definitions": "^5.0.0", "micromark-util-sanitize-uri": "^1.1.0", "trim-lines": "^3.0.0", "unist-util-generated": "^2.0.0", "unist-util-position": "^4.0.0", "unist-util-visit": "^4.0.0" } }, "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw=="],
|
||||
|
||||
"solidjs-markdown/unified/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/vfile/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="],
|
||||
|
||||
"tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/@types/mdast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/mdast-util-to-string": ["mdast-util-to-string@3.2.0", "", { "dependencies": { "@types/mdast": "^3.0.0" } }, "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark": ["micromark@3.2.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "micromark-core-commonmark": "^1.0.1", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-combine-extensions": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-sanitize-uri": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-decode-string": ["micromark-util-decode-string@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-symbol": ["micromark-util-symbol@1.1.0", "", {}, "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/@types/mdast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast/micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@1.2.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark": ["micromark-core-commonmark@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-factory-destination": "^1.0.0", "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", "micromark-factory-title": "^1.0.0", "micromark-factory-whitespace": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-classify-character": "^1.0.0", "micromark-util-html-tag-name": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-factory-space": ["micromark-factory-space@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-chunked": ["micromark-util-chunked@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-combine-extensions": ["micromark-util-combine-extensions@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-encode": ["micromark-util-encode@1.1.0", "", {}, "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-resolve-all": ["micromark-util-resolve-all@1.1.0", "", { "dependencies": { "micromark-util-types": "^1.0.0" } }, "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@1.2.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-util-subtokenize": ["micromark-util-subtokenize@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark-util-decode-string/micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast/micromark-util-sanitize-uri/micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast/micromark-util-sanitize-uri/micromark-util-encode": ["micromark-util-encode@1.1.0", "", {}, "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast/micromark-util-sanitize-uri/micromark-util-symbol": ["micromark-util-symbol@1.1.0", "", {}, "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-factory-destination": ["micromark-factory-destination@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-factory-label": ["micromark-factory-label@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-factory-title": ["micromark-factory-title@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-factory-whitespace": ["micromark-factory-whitespace@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-util-classify-character": ["micromark-util-classify-character@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw=="],
|
||||
|
||||
"solidjs-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark/micromark-util-html-tag-name": ["micromark-util-html-tag-name@1.2.0", "", {}, "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q=="],
|
||||
|
||||
"solidjs-markdown/remark-rehype/mdast-util-to-hast/micromark-util-sanitize-uri/micromark-util-character/micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="],
|
||||
}
|
||||
}
|
||||
BIN
frontend/bun.lockb
Executable file
BIN
frontend/bun.lockb
Executable file
Binary file not shown.
17
frontend/index.html
Normal file
17
frontend/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||
<title>Tauri + Solid + Typescript App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
frontend/package.json
Normal file
54
frontend/package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "haystack",
|
||||
"version": "0.1.0",
|
||||
"description": "Screenshots that organize themselves",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "bunx @biomejs/biome lint .",
|
||||
"format": "bunx @biomejs/biome format . --write"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "^0.13.11",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tabler/icons-solidjs": "^3.35.0",
|
||||
"@tanstack/solid-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"@tauri-apps/plugin-dialog": "~2.3.3",
|
||||
"@tauri-apps/plugin-fs": "~2.4.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.2",
|
||||
"@tauri-apps/plugin-log": "^2.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"solid-js": "^1.9.9",
|
||||
"solid-markdown": "^2.0.14",
|
||||
"solid-motionone": "^1.0.4",
|
||||
"solidjs-markdown": "^0.2.0",
|
||||
"tailwind-scrollbar-hide": "^2.0.0",
|
||||
"tauri-plugin-ios-shared-token-api": "file:../tauri-plugin-ios-shared-token",
|
||||
"tauri-plugin-sharetarget-api": "^0.1.6",
|
||||
"valibot": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@tauri-apps/cli": "^2.8.4",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"tailwindcss": "3.4.0",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-solid": "^2.11.8",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
6
frontend/public/tauri.svg
Normal file
6
frontend/public/tauri.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/src-tauri/.DS_Store
vendored
Normal file
BIN
frontend/src-tauri/.DS_Store
vendored
Normal file
Binary file not shown.
7
frontend/src-tauri/.gitignore
vendored
Normal file
7
frontend/src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
5904
frontend/src-tauri/Cargo.lock
generated
Normal file
5904
frontend/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
frontend/src-tauri/Cargo.toml
Normal file
45
frontend/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "Haystack"
|
||||
version = "0.1.0"
|
||||
description = "Screenshots that organize themselves"
|
||||
authors = ["Dmytro Kondakov", "John Costa"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "haystack_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["macos-private-api", "tray-icon"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
notify = "6.1.1"
|
||||
base64 = "0.21.7"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
tauri-plugin-http = "2.4.3"
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-store = "2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
cocoa = "0.26"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\"))".dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies]
|
||||
tauri-plugin-sharetarget = "0.1.6"
|
||||
|
||||
[target."cfg(any(target_os = \"ios\"))".dependencies]
|
||||
tauri-plugin-ios-shared-token = { path = "../../tauri-plugin-ios-shared-token"}
|
||||
3
frontend/src-tauri/build.rs
Normal file
3
frontend/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
8
frontend/src-tauri/capabilities/android.toml
Normal file
8
frontend/src-tauri/capabilities/android.toml
Normal file
@ -0,0 +1,8 @@
|
||||
identifier = "android"
|
||||
description = "Capabilities for Android platforms"
|
||||
windows = ["main"]
|
||||
platforms = ["android"]
|
||||
|
||||
permissions = [
|
||||
"sharetarget:default"
|
||||
]
|
||||
18
frontend/src-tauri/capabilities/desktop.toml
Normal file
18
frontend/src-tauri/capabilities/desktop.toml
Normal file
@ -0,0 +1,18 @@
|
||||
identifier = "Desktop"
|
||||
description = "Capabilities for desktop platforms"
|
||||
windows = ["main"]
|
||||
platforms = ["linux", "macOS", "windows"]
|
||||
|
||||
permissions = [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"fs:default",
|
||||
"os:default",
|
||||
{ identifier = "http:default", allow = [
|
||||
{ url = "https://haystack.johncosta.tech" },
|
||||
{ url = "http://localhost:3040" },
|
||||
{ url = "http://192.168.1.199:3040" }
|
||||
] },
|
||||
]
|
||||
8
frontend/src-tauri/capabilities/ios.toml
Normal file
8
frontend/src-tauri/capabilities/ios.toml
Normal file
@ -0,0 +1,8 @@
|
||||
identifier = "iOS"
|
||||
description = "Capabilities for iOS platforms"
|
||||
windows = ["main"]
|
||||
platforms = ["iOS"]
|
||||
|
||||
permissions = [
|
||||
"ios-shared-token:default"
|
||||
]
|
||||
16
frontend/src-tauri/capabilities/mobile.toml
Normal file
16
frontend/src-tauri/capabilities/mobile.toml
Normal file
@ -0,0 +1,16 @@
|
||||
identifier = "Mobile"
|
||||
description = "Capabilities for mobile platforms"
|
||||
windows = ["main"]
|
||||
platforms = ["android", "iOS"]
|
||||
|
||||
permissions = [
|
||||
"core:default",
|
||||
"fs:default",
|
||||
"http:default",
|
||||
"os:default",
|
||||
{ identifier = "http:default", allow = [
|
||||
{ url = "https://haystack.johncosta.tech" },
|
||||
{ url = "http://localhost:3040" },
|
||||
{ url = "http://192.168.1.199:3040" }
|
||||
] },
|
||||
]
|
||||
12
frontend/src-tauri/gen/android/.editorconfig
Normal file
12
frontend/src-tauri/gen/android/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
frontend/src-tauri/gen/android/.gitignore
vendored
Normal file
19
frontend/src-tauri/gen/android/.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
7
frontend/src-tauri/gen/android/app/.gitignore
vendored
Normal file
7
frontend/src-tauri/gen/android/app/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/src/main/java/com/haystack/app/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
upload_certificate.pem
|
||||
83
frontend/src-tauri/gen/android/app/build.gradle.kts
Normal file
83
frontend/src-tauri/gen/android/app/build.gradle.kts
Normal file
@ -0,0 +1,83 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keyPropertiesFile = rootProject.file("key.properties")
|
||||
val keyProperties = Properties()
|
||||
keyProperties.load(FileInputStream(keyPropertiesFile))
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
namespace = "com.haystack.app"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "com.screenshot.haystack.app"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keyProperties["keyAlias"] as String
|
||||
keyPassword = keyProperties["keyPassword"] as String
|
||||
storeFile = file(keyProperties["storeFile"] as String)
|
||||
storePassword = keyProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
21
frontend/src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
21
frontend/src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.haystack"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<data android:mimeType="image/*" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@ -0,0 +1,3 @@
|
||||
package com.haystack.app
|
||||
|
||||
class MainActivity : TauriActivity()
|
||||
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user