Compare commits
227 Commits
feat/agent
...
9407f54677
Author | SHA1 | Date | |
---|---|---|---|
9407f54677 | |||
2bcf06f5c6 | |||
ecd59ec814 | |||
0ef20264c1 | |||
0c2c8bde74 | |||
bb4760036e | |||
3e8df1ba6f | |||
1c265d8a60 | |||
4f2b78b9f1 | |||
d935d6a8b9 | |||
03656cf42e | |||
e735aca168 | |||
8ea0d53af7 | |||
8850b00595 | |||
e0fadb2c66 | |||
c3fc915e60 | |||
55cd552724 | |||
981bca86e9 | |||
ac0bcfdae0 | |||
af2aa2c1b6 | |||
9f98a21532 | |||
130bce86a1 | |||
4da37d1704 | |||
016834ee7d | |||
bae6a7eda9 | |||
e706b6a976 | |||
7e7f01447e | |||
dc202189ca | |||
1cff278263 | |||
1e40390952 | |||
150a43a5dc | |||
2b7206c29e | |||
7002b05aae | |||
8e73ad6f4e | |||
4b0ef8b17f | |||
57c760e7f0 | |||
f5fdaff7c1 | |||
8fff043849 | |||
d1fd2aeaf1 | |||
1e5028177f | |||
c4569e925b | |||
8fed2f9b9a | |||
1651926c4d | |||
fa127c2331 | |||
7be669e49e | |||
7b6bdf2c7b | |||
7b40959125 | |||
63201280bb | |||
885f877ef0 | |||
f38d44f5f5 | |||
64f8aa032e | |||
4ba41258e0 | |||
49f5cd0afb | |||
3ccb7ad7a1 | |||
2e092e5fe4 | |||
60273c5782 | |||
98799b01e6 | |||
1d0eb8ddaa | |||
8e7ee204ce | |||
0ff541b7b6 | |||
732d0cedd0 | |||
73dad6fd2d | |||
cb3b930c32 | |||
cbf013aece | |||
edcba60c5a | |||
ba940ae6fd | |||
efedb4e63c | |||
9ac569359c | |||
b13d3c1881 | |||
e1857bd532 | |||
b1b46ff7e5 | |||
dbb98d1e48 | |||
d3fb92546f | |||
313b764ec4 | |||
43e63f739a | |||
ce9e27ec68 | |||
76618d1124 | |||
22a6ad9818 | |||
8af4f62492 | |||
6e96eb53b4 | |||
ca2e98e4b4 | |||
43404aaf18 | |||
4196952178 | |||
767ca20b4c | |||
bda733f8a5 | |||
7d68f39bab | |||
d687e86f86 | |||
fca4a6445c | |||
17cc12f0c9 | |||
b57968b938 | |||
f0477ed720 | |||
f09cc137a3 | |||
4e78d2e701 | |||
0b97b2eed2 | |||
0fcdd73a47 | |||
35072684de | |||
391d0fdde2 | |||
ab86bff35f | |||
1560d1504e | |||
ae9ac1fe4f | |||
ca8583575e | |||
02490c6c84 | |||
36e776789e | |||
df028aaedb | |||
70d4411270 | |||
6b181aac9f | |||
324aac438b | |||
b0f4a45c40 | |||
c02ccfd274 | |||
7264c6ed32 | |||
283265c8c5 | |||
5b03d38392 | |||
207f263853 | |||
5b7fdd9f3e | |||
4dbe1508c2 | |||
36d60d7985 | |||
a86addc8b2 | |||
06d2f1db6e | |||
b209de5c5d | |||
680003b626 | |||
1a9b707533 | |||
c35951063a | |||
f294f9cdc0 | |||
88fda32125 | |||
5502fc6b19 | |||
28ee32e2ff | |||
f5f4008034 | |||
d474b1700a | |||
d78f34a7aa | |||
1cafc31e0a | |||
a1ce96d2e3 | |||
03e7803467 | |||
286a9a8472 | |||
aa153de185 | |||
cd27f1105a | |||
71d4581110 | |||
8a165c2042 | |||
fe7c92b622 | |||
745265773b | |||
f72ee73020 | |||
1cb6510465 | |||
af485aec49 | |||
4320bd7fe9 | |||
9652549b01 | |||
b56250c1f8 | |||
35f004752d | |||
901f214f9d | |||
0c78d741f0 | |||
7c5d2f9433 | |||
e29f93bcd5 | |||
13e82334ca | |||
59737ca9ac | |||
b6969127eb | |||
2645cbb1c2 | |||
d716e66463 | |||
a576355e7c | |||
2dcb59c19d | |||
2c279bb68f | |||
6f938a34e3 | |||
7debe6bab2 | |||
4c4bf7a9e4 | |||
aad45fcf52 | |||
7c473e054a | |||
7f96d2fc45 | |||
df90eaa6ad | |||
44d506bc69 | |||
45f3e11214 | |||
58f7afb521 | |||
7b64563647 | |||
5b794b2e7f | |||
72a3e58ef9 | |||
f042c9dfcc | |||
d2dd43c6b2 | |||
56d423d261 | |||
072eebc0bf | |||
28dd02a47d | |||
2e1809aa27 | |||
e76a4b901c | |||
b359e9d61d | |||
9b5113f428 | |||
b29a013cde | |||
32b5b08dcf | |||
439f729150 | |||
bfd6d136dc | |||
1d5d90c3b5 | |||
b8ee0c2381 | |||
d9f972f674 | |||
d4c6aa0310 | |||
6fcb1e9f26 | |||
c215bf6909 | |||
234988399d | |||
aee49c313b | |||
9e3896a30f | |||
e5ac7061f4 | |||
03a4d49ee6 | |||
c025eebea8 | |||
53c4ec1869 | |||
5192aeb70f | |||
3bd3b420c9 | |||
06df315ed0 | |||
a3dee09011 | |||
a4136fe3f8 | |||
0ec2ae65d2 | |||
79ee5e23ca | |||
1f16cfb30b | |||
2a37e37c4b | |||
b29f5b1bb5 | |||
111c6ac74f | |||
24fc2d7c84 | |||
c36fb1c0d6 | |||
e26835861d | |||
e69d7b5c08 | |||
c0fe7e1853 | |||
c9cd0df9ca | |||
18ecfecd54 | |||
51d14b0eba | |||
4a32e99a5c | |||
098fd05dfd | |||
fe786690ad | |||
ac8d3387c5 | |||
ec13e70024 | |||
40debf1da3 | |||
b773abd51a | |||
050126116c | |||
49680c00b2 | |||
cf452653a7 | |||
531b126cf5 |
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.
|
@ -1,18 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 SystemPrompts struct {
|
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
|
||||||
Prompt string
|
|
||||||
AgentID uuid.UUID
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 Tools struct {
|
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
|
||||||
Tool string
|
|
||||||
AgentID uuid.UUID
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 Agents = newAgentsTable("agents", "agents", "")
|
|
||||||
|
|
||||||
type agentsTable struct {
|
|
||||||
postgres.Table
|
|
||||||
|
|
||||||
// Columns
|
|
||||||
ID postgres.ColumnString
|
|
||||||
Name postgres.ColumnString
|
|
||||||
|
|
||||||
AllColumns postgres.ColumnList
|
|
||||||
MutableColumns postgres.ColumnList
|
|
||||||
}
|
|
||||||
|
|
||||||
type AgentsTable struct {
|
|
||||||
agentsTable
|
|
||||||
|
|
||||||
EXCLUDED agentsTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// AS creates new AgentsTable with assigned alias
|
|
||||||
func (a AgentsTable) AS(alias string) *AgentsTable {
|
|
||||||
return newAgentsTable(a.SchemaName(), a.TableName(), alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema creates new AgentsTable with assigned schema name
|
|
||||||
func (a AgentsTable) FromSchema(schemaName string) *AgentsTable {
|
|
||||||
return newAgentsTable(schemaName, a.TableName(), a.Alias())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPrefix creates new AgentsTable with assigned table prefix
|
|
||||||
func (a AgentsTable) WithPrefix(prefix string) *AgentsTable {
|
|
||||||
return newAgentsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSuffix creates new AgentsTable with assigned table suffix
|
|
||||||
func (a AgentsTable) WithSuffix(suffix string) *AgentsTable {
|
|
||||||
return newAgentsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAgentsTable(schemaName, tableName, alias string) *AgentsTable {
|
|
||||||
return &AgentsTable{
|
|
||||||
agentsTable: newAgentsTableImpl(schemaName, tableName, alias),
|
|
||||||
EXCLUDED: newAgentsTableImpl("", "excluded", ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAgentsTableImpl(schemaName, tableName, alias string) agentsTable {
|
|
||||||
var (
|
|
||||||
IDColumn = postgres.StringColumn("id")
|
|
||||||
NameColumn = postgres.StringColumn("name")
|
|
||||||
allColumns = postgres.ColumnList{IDColumn, NameColumn}
|
|
||||||
mutableColumns = postgres.ColumnList{NameColumn}
|
|
||||||
)
|
|
||||||
|
|
||||||
return agentsTable{
|
|
||||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
|
||||||
|
|
||||||
//Columns
|
|
||||||
ID: IDColumn,
|
|
||||||
Name: NameColumn,
|
|
||||||
|
|
||||||
AllColumns: allColumns,
|
|
||||||
MutableColumns: mutableColumns,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 SystemPrompts = newSystemPromptsTable("agents", "system_prompts", "")
|
|
||||||
|
|
||||||
type systemPromptsTable struct {
|
|
||||||
postgres.Table
|
|
||||||
|
|
||||||
// Columns
|
|
||||||
ID postgres.ColumnString
|
|
||||||
Prompt postgres.ColumnString
|
|
||||||
AgentID postgres.ColumnString
|
|
||||||
|
|
||||||
AllColumns postgres.ColumnList
|
|
||||||
MutableColumns postgres.ColumnList
|
|
||||||
}
|
|
||||||
|
|
||||||
type SystemPromptsTable struct {
|
|
||||||
systemPromptsTable
|
|
||||||
|
|
||||||
EXCLUDED systemPromptsTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// AS creates new SystemPromptsTable with assigned alias
|
|
||||||
func (a SystemPromptsTable) AS(alias string) *SystemPromptsTable {
|
|
||||||
return newSystemPromptsTable(a.SchemaName(), a.TableName(), alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema creates new SystemPromptsTable with assigned schema name
|
|
||||||
func (a SystemPromptsTable) FromSchema(schemaName string) *SystemPromptsTable {
|
|
||||||
return newSystemPromptsTable(schemaName, a.TableName(), a.Alias())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPrefix creates new SystemPromptsTable with assigned table prefix
|
|
||||||
func (a SystemPromptsTable) WithPrefix(prefix string) *SystemPromptsTable {
|
|
||||||
return newSystemPromptsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSuffix creates new SystemPromptsTable with assigned table suffix
|
|
||||||
func (a SystemPromptsTable) WithSuffix(suffix string) *SystemPromptsTable {
|
|
||||||
return newSystemPromptsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSystemPromptsTable(schemaName, tableName, alias string) *SystemPromptsTable {
|
|
||||||
return &SystemPromptsTable{
|
|
||||||
systemPromptsTable: newSystemPromptsTableImpl(schemaName, tableName, alias),
|
|
||||||
EXCLUDED: newSystemPromptsTableImpl("", "excluded", ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSystemPromptsTableImpl(schemaName, tableName, alias string) systemPromptsTable {
|
|
||||||
var (
|
|
||||||
IDColumn = postgres.StringColumn("id")
|
|
||||||
PromptColumn = postgres.StringColumn("prompt")
|
|
||||||
AgentIDColumn = postgres.StringColumn("agent_id")
|
|
||||||
allColumns = postgres.ColumnList{IDColumn, PromptColumn, AgentIDColumn}
|
|
||||||
mutableColumns = postgres.ColumnList{PromptColumn, AgentIDColumn}
|
|
||||||
)
|
|
||||||
|
|
||||||
return systemPromptsTable{
|
|
||||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
|
||||||
|
|
||||||
//Columns
|
|
||||||
ID: IDColumn,
|
|
||||||
Prompt: PromptColumn,
|
|
||||||
AgentID: AgentIDColumn,
|
|
||||||
|
|
||||||
AllColumns: allColumns,
|
|
||||||
MutableColumns: mutableColumns,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
//
|
|
||||||
// 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) {
|
|
||||||
Agents = Agents.FromSchema(schema)
|
|
||||||
SystemPrompts = SystemPrompts.FromSchema(schema)
|
|
||||||
Tools = Tools.FromSchema(schema)
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 Tools = newToolsTable("agents", "tools", "")
|
|
||||||
|
|
||||||
type toolsTable struct {
|
|
||||||
postgres.Table
|
|
||||||
|
|
||||||
// Columns
|
|
||||||
ID postgres.ColumnString
|
|
||||||
Tool postgres.ColumnString
|
|
||||||
AgentID postgres.ColumnString
|
|
||||||
|
|
||||||
AllColumns postgres.ColumnList
|
|
||||||
MutableColumns postgres.ColumnList
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolsTable struct {
|
|
||||||
toolsTable
|
|
||||||
|
|
||||||
EXCLUDED toolsTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// AS creates new ToolsTable with assigned alias
|
|
||||||
func (a ToolsTable) AS(alias string) *ToolsTable {
|
|
||||||
return newToolsTable(a.SchemaName(), a.TableName(), alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema creates new ToolsTable with assigned schema name
|
|
||||||
func (a ToolsTable) FromSchema(schemaName string) *ToolsTable {
|
|
||||||
return newToolsTable(schemaName, a.TableName(), a.Alias())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPrefix creates new ToolsTable with assigned table prefix
|
|
||||||
func (a ToolsTable) WithPrefix(prefix string) *ToolsTable {
|
|
||||||
return newToolsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSuffix creates new ToolsTable with assigned table suffix
|
|
||||||
func (a ToolsTable) WithSuffix(suffix string) *ToolsTable {
|
|
||||||
return newToolsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newToolsTable(schemaName, tableName, alias string) *ToolsTable {
|
|
||||||
return &ToolsTable{
|
|
||||||
toolsTable: newToolsTableImpl(schemaName, tableName, alias),
|
|
||||||
EXCLUDED: newToolsTableImpl("", "excluded", ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newToolsTableImpl(schemaName, tableName, alias string) toolsTable {
|
|
||||||
var (
|
|
||||||
IDColumn = postgres.StringColumn("id")
|
|
||||||
ToolColumn = postgres.StringColumn("tool")
|
|
||||||
AgentIDColumn = postgres.StringColumn("agent_id")
|
|
||||||
allColumns = postgres.ColumnList{IDColumn, ToolColumn, AgentIDColumn}
|
|
||||||
mutableColumns = postgres.ColumnList{ToolColumn, AgentIDColumn}
|
|
||||||
)
|
|
||||||
|
|
||||||
return toolsTable{
|
|
||||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
|
||||||
|
|
||||||
//Columns
|
|
||||||
ID: IDColumn,
|
|
||||||
Tool: ToolColumn,
|
|
||||||
AgentID: AgentIDColumn,
|
|
||||||
|
|
||||||
AllColumns: allColumns,
|
|
||||||
MutableColumns: mutableColumns,
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Agents struct {
|
type Logs struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
Log string
|
||||||
Name string
|
ImageID uuid.UUID
|
||||||
}
|
}
|
78
backend/.gen/haystack/haystack/table/logs.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// 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 Logs = newLogsTable("haystack", "logs", "")
|
||||||
|
|
||||||
|
type logsTable struct {
|
||||||
|
postgres.Table
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
Log postgres.ColumnString
|
||||||
|
ImageID postgres.ColumnString
|
||||||
|
|
||||||
|
AllColumns postgres.ColumnList
|
||||||
|
MutableColumns postgres.ColumnList
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsTable struct {
|
||||||
|
logsTable
|
||||||
|
|
||||||
|
EXCLUDED logsTable
|
||||||
|
}
|
||||||
|
|
||||||
|
// AS creates new LogsTable with assigned alias
|
||||||
|
func (a LogsTable) AS(alias string) *LogsTable {
|
||||||
|
return newLogsTable(a.SchemaName(), a.TableName(), alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema creates new LogsTable with assigned schema name
|
||||||
|
func (a LogsTable) FromSchema(schemaName string) *LogsTable {
|
||||||
|
return newLogsTable(schemaName, a.TableName(), a.Alias())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix creates new LogsTable with assigned table prefix
|
||||||
|
func (a LogsTable) WithPrefix(prefix string) *LogsTable {
|
||||||
|
return newLogsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSuffix creates new LogsTable with assigned table suffix
|
||||||
|
func (a LogsTable) WithSuffix(suffix string) *LogsTable {
|
||||||
|
return newLogsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogsTable(schemaName, tableName, alias string) *LogsTable {
|
||||||
|
return &LogsTable{
|
||||||
|
logsTable: newLogsTableImpl(schemaName, tableName, alias),
|
||||||
|
EXCLUDED: newLogsTableImpl("", "excluded", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
|
||||||
|
var (
|
||||||
|
LogColumn = postgres.StringColumn("log")
|
||||||
|
ImageIDColumn = postgres.StringColumn("image_id")
|
||||||
|
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
|
||||||
|
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
|
||||||
|
)
|
||||||
|
|
||||||
|
return logsTable{
|
||||||
|
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||||
|
|
||||||
|
//Columns
|
||||||
|
Log: LogColumn,
|
||||||
|
ImageID: ImageIDColumn,
|
||||||
|
|
||||||
|
AllColumns: allColumns,
|
||||||
|
MutableColumns: mutableColumns,
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ func UseSchema(schema string) {
|
|||||||
ImageTags = ImageTags.FromSchema(schema)
|
ImageTags = ImageTags.FromSchema(schema)
|
||||||
ImageText = ImageText.FromSchema(schema)
|
ImageText = ImageText.FromSchema(schema)
|
||||||
Locations = Locations.FromSchema(schema)
|
Locations = Locations.FromSchema(schema)
|
||||||
|
Logs = Logs.FromSchema(schema)
|
||||||
Notes = Notes.FromSchema(schema)
|
Notes = Notes.FromSchema(schema)
|
||||||
UserContacts = UserContacts.FromSchema(schema)
|
UserContacts = UserContacts.FromSchema(schema)
|
||||||
UserEvents = UserEvents.FromSchema(schema)
|
UserEvents = UserEvents.FromSchema(schema)
|
||||||
|
@ -12,51 +12,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const contactPrompt = `
|
const contactPrompt = `
|
||||||
**Role:** You are an AI assistant specialized in processing contact information from images. Your primary function is to use the provided tools (listContacts, createContact, stopAgent) to manage contacts based on image analysis and signal when processing is complete.
|
**Role:** AI Contact Processor from Images.
|
||||||
|
|
||||||
**Primary Goal:** To accurately identify potential contacts in an image, check against existing contacts using the provided tools, create new contact entries when necessary (meticulously avoiding duplicates), and explicitly stop processing when finished or if no action is needed.
|
**Goal:** Extract contacts from an image, check against existing list using listContacts, add *only* new contacts using createContact, and call stopAgent when finished. Avoid duplicates.
|
||||||
|
|
||||||
**Input:** You will be given an image that may contain contact information.
|
**Input:** Image potentially containing contact info (Name, Phone, Email, Address).
|
||||||
|
|
||||||
**Output Behavior (CRITICAL):**
|
**Workflow:**
|
||||||
* **If providing a text response:** Generate only the conversational text intended for the user in the response content. (Note: This should generally not happen in this workflow, as actions are handled by tools).
|
1. **Scan Image:** Extract all contact details. If none, call stopAgent.
|
||||||
* **If using a tool:** Generate **only** the structured tool call request in the designated tool call section of the response. **Do NOT include the tool call JSON, parameters, or any description of your intention to call the tool within the main text/content response.** Your output must be strictly one or the other for a given turn: either text content OR a tool call structure.
|
2. **Think:** Using the think tool, you must layout your thoughts about the contacts on the image. If they are duplicates or not, and what your next action should be,
|
||||||
|
3. **Check Duplicates:** If contacts found, *first* call listContacts. Compare extracted info to list. If all found contacts already exist, call stopAgent.
|
||||||
|
4. **Add New:** If you detect a new contact on the image, call createContact to create a new contact.
|
||||||
|
5. **Finish:** Call stopAgent once all new contacts are created OR if steps 1 or 2 determined no action/creation was needed.
|
||||||
|
|
||||||
**Core Workflow:**
|
**Tools:**
|
||||||
|
* listContacts: Check existing contacts (Use first if contacts found).
|
||||||
1. **Image Analysis:**
|
* createContact: Add a NEW contact (Name required).
|
||||||
* Carefully scan the provided image to identify and extract any visible contact details (Name, Phone Number, Email Address, Physical Address). Extract *all* available information for each potential contact.
|
* stopAgent: Signal task completion.
|
||||||
* **If NO potential contact information is found in the image, proceed directly to Step 5 (call stopAgent).**
|
|
||||||
|
|
||||||
2. **Duplicate Check (Mandatory First Step if contacts found):**
|
|
||||||
* If potential contact(s) were found in Step 1, you **must** call the listContacts tool first. **Generate only the listContacts tool call structure.**
|
|
||||||
* Once you receive the list, compare the extracted information against the existing contacts to determine if each identified person is already present.
|
|
||||||
* **If *all* identified potential contacts already exist in the list, proceed directly to Step 5 (call stopAgent).**
|
|
||||||
|
|
||||||
3. **Create New Contact (Conditional):**
|
|
||||||
* For each potential contact identified in Step 1 that your check in Step 2 confirms is *new*:
|
|
||||||
* Call the createContact tool with *all* corresponding extracted information (name, phoneNumber, address, email). name is mandatory. **Generate only the createContact tool call structure.**
|
|
||||||
* Process *one new contact creation per turn*. If multiple new contacts need creating, you will call createContact sequentially (one call per turn).
|
|
||||||
|
|
||||||
4. **Handling Multiple Contacts:**
|
|
||||||
* The workflow intrinsically handles multiple contacts by requiring a listContacts check first, followed by potential sequential createContact calls for each new individual found.
|
|
||||||
|
|
||||||
5. **Task Completion / No Action Needed:**
|
|
||||||
* Call the stopAgent tool **only** when one of the following conditions is met:
|
|
||||||
* No potential contact information was found in the initial image analysis (Step 1).
|
|
||||||
* The listContacts check confirmed that *all* potential contacts identified in the image already exist (Step 2).
|
|
||||||
* You have successfully processed all identified contacts (i.e., performed the listContacts check and called createContact for *all* new individuals found).
|
|
||||||
* **Generate only the stopAgent tool call structure.**
|
|
||||||
|
|
||||||
**Available Tools:**
|
|
||||||
|
|
||||||
* **listContacts**: Retrieves the existing contact list. **Must** be called first if potential contacts are found in the image, to enable duplicate checking.
|
|
||||||
* **createContact**: Adds a *new*, non-duplicate contact. Only call *after* listContacts confirms the person is new. name is mandatory.
|
|
||||||
* **stopAgent**: Signals that processing for the current image is complete (either action was taken, no action was needed, or all identified contacts already existed). Call this as the final step or when no other action is applicable based on the workflow.
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const contactTools = `
|
const contactTools = `
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "think",
|
||||||
|
"description": "Use this tool to think through the image, evaluating the contact and whether or not it exists in the users listContacts.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"thought": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A singular thought about the image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["thought"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -134,6 +127,10 @@ func NewContactAgent(log *log.Logger, contactModel models.ContactModel) client.A
|
|||||||
EndToolCall: "stopAgent",
|
EndToolCall: "stopAgent",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
|
return "Thought", nil
|
||||||
|
})
|
||||||
|
|
||||||
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
return contactModel.List(context.Background(), info.UserId)
|
return contactModel.List(context.Background(), info.UserId)
|
||||||
})
|
})
|
||||||
|
@ -13,47 +13,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const eventPrompt = `
|
const eventPrompt = `
|
||||||
**Role:** You are an Event Processing AI Assistant specialized in extracting event information from images, managing event data using provided tools, and ensuring accuracy and avoiding duplicates.
|
**You are an AI processing events from images using internal thought.**
|
||||||
|
|
||||||
**Primary Goal:** To analyze images, identify potential events (like meetings, appointments, conferences, invitations), extract key details (name, date/time, location description), check against existing events, retrieve location identifiers if applicable, create new event entries when necessary, and signal completion using the 'finish' tool.
|
**Task:** Extract event details (Name, Date/Time, Location). Use think before deciding actions. Check duplicates with listEvents. Handle new events via getEventLocationId (if location exists) and createEvent. Use finish if no event or duplicate found.
|
||||||
|
1. **Analyze Image & Think:** Extract details. Use think to confirm if a valid event exists. If not -> stopAgent.
|
||||||
|
2. **Event Confirmed?** -> *Must* call listEvents, to check for existing events and prevent duplicates.
|
||||||
|
3. **Detect Duplicates** -> If the input contains an event that already exists from listEvents, then you should call stopAgent.
|
||||||
|
4. **New Events**
|
||||||
|
* If you think the input contains a location, then you can use getEventLocationId to retrieve the ID of the location. Only use this IF the input contains a location.
|
||||||
|
* Call createEvent.
|
||||||
|
5. **Multiple Events:** Process sequentially using this logic.
|
||||||
|
|
||||||
**Core Workflow:**
|
**Tools:**
|
||||||
|
* think: Internal reasoning/planning step.
|
||||||
**Duplicate Check (Mandatory if Event Found):**
|
* listEvents: Check for duplicates (mandatory first step for found events).
|
||||||
* If potential event details were found, you **must** call the listEvents tool first to check for duplicates. **Generate only the listEvents tool call structure.**
|
* getEventLocationId: Get ID for location text.
|
||||||
* Once you receive the list, compare the extracted event details (Name, Start Date/Time primarily) against the existing events.
|
* createEvent: Add new event (Name req.). Terminal action for new events.
|
||||||
* **If a matching event already exists, proceed directly to Step 6 (call finish).**
|
* stopAgent: Signal completion (no event/duplicate found). Terminal action.
|
||||||
|
|
||||||
**Location ID Retrieval (Conditional):**
|
|
||||||
* If the event is identified as *new* AND a *location description* was extracted.
|
|
||||||
* Call the getEventLocationId tool, providing the extracted location description. **Generate only the getEventLocationId tool call structure.**
|
|
||||||
|
|
||||||
**Create Event:**
|
|
||||||
* If the event was identified as *new*:
|
|
||||||
* Prepare the parameters for the createEvent tool using the extracted details (Name, Start Date/Time, End Date/Time).
|
|
||||||
* If you identify the event as *duplicate*, meaning you think an event in listEvents is the same as the event on this image.
|
|
||||||
* Call the updateEvent tool so this image is also linked to that event. If you find any new information you can update it using this tool too.
|
|
||||||
|
|
||||||
**Handling Multiple Events:**
|
|
||||||
* If the image contains multiple distinct events, ideally process them one by one.
|
|
||||||
* Do this until there are no more events on this image
|
|
||||||
|
|
||||||
**Task Completion / No Action Needed:**
|
|
||||||
* Call the finish tool **only** when one of the following conditions is met:
|
|
||||||
* No identifiable event information was found in the initial image analysis.
|
|
||||||
* The listEvents check confirmed the identified event already exists.
|
|
||||||
* You have successfully called createEvent for a new event.
|
|
||||||
|
|
||||||
**Available Tools:**
|
|
||||||
|
|
||||||
* **listEvents**: Retrieves the user's existing events. **Must** be called first if potential event details are found in the image, to enable duplicate checking.
|
|
||||||
* **getEventLocationId**: Takes a location description (text) and retrieves a unique ID (locationId) for it. Use this *before* createEvent *only* if a new event has a specific location mentioned.
|
|
||||||
* **createEvent**: Adds a *new*, non-duplicate event to the user's calendar/list. Only call *after* listEvents confirms the event is new. Requires name. Include startDateTime, endDateTime, and locationId (if available and retrieved).
|
|
||||||
* **stopAgent**: Signals that processing for the current image is complete (either action was taken, no action was needed because the event already existed, or no event was found). Call this as the final step.
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const eventTools = `
|
const eventTools = `
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"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",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -168,6 +165,10 @@ func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel
|
|||||||
locationQuery := "Can you get me the ID of the location present in this image?"
|
locationQuery := "Can you get me the ID of the location present in this image?"
|
||||||
locationAgent.Options.Query = &locationQuery
|
locationAgent.Options.Query = &locationQuery
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
|
return "Thought", nil
|
||||||
|
})
|
||||||
|
|
||||||
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
return eventsModel.List(context.Background(), info.UserId)
|
return eventsModel.List(context.Background(), info.UserId)
|
||||||
})
|
})
|
||||||
@ -183,6 +184,8 @@ func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel
|
|||||||
|
|
||||||
layout := "2006-01-02T15:04:05Z"
|
layout := "2006-01-02T15:04:05Z"
|
||||||
|
|
||||||
|
// TODO: check for nil pointers.
|
||||||
|
|
||||||
startTime, err := time.Parse(layout, *args.StartDateTime)
|
startTime, err := time.Parse(layout, *args.StartDateTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Events{}, err
|
return model.Events{}, err
|
||||||
|
@ -15,32 +15,35 @@ import (
|
|||||||
const locationPrompt = `
|
const locationPrompt = `
|
||||||
Role: Location AI Assistant
|
Role: Location AI Assistant
|
||||||
|
|
||||||
Objective: Identify locations from images/text, manage a saved list (create, update), and answer user queries about saved locations using the provided tools.
|
Objective: Identify locations from images/text, manage a saved list, and answer user queries about saved locations using the provided tools.
|
||||||
|
The user does not want to have duplicate entries on their saved location list. So you should only create a new location if listLocation doesnt return
|
||||||
|
what would be a duplicate.
|
||||||
|
|
||||||
Core Logic:
|
Core Logic:
|
||||||
|
|
||||||
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input (image or text).
|
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input.
|
||||||
* If no details can be extracted, inform the user and use stopAgent.
|
* If no details can be extracted, inform the user and use stopAgent.
|
||||||
|
|
||||||
**Check for Existing Location:** If details *were* extracted:
|
**Check for Existing Location:** If details *were* extracted:
|
||||||
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
|
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
|
||||||
|
|
||||||
|
Action loop:
|
||||||
|
**Thinking**
|
||||||
|
* Use the think tool to analytise the image.
|
||||||
|
* You should think about whether listLocations already contains this location, or if it is a new location.
|
||||||
|
* You should always call this after listLocations.
|
||||||
|
* You must think about whether or not listLocations already has this location.
|
||||||
|
|
||||||
**Decide Action based on Search Results:**
|
**Decide Action based on Search Results:**
|
||||||
* **If listLocations returns one or more likely matches:**
|
* If no existing location looks like the location on the input. You should use createLocation.
|
||||||
* Identify the *best* match (based on name, address similarity).
|
* Do not use this tool if this location already exists.
|
||||||
* **Crucially:** Call upsertLocation, providing the locationId of that best match. Include the newly extracted InputName (required) and any other extracted details (InputAddress, etc.) to potentially *update* the existing record or simply link the current input to it.
|
* If the input contains a location that already exists, you should use createExistingLocation.
|
||||||
* **If listLocations returns no matches OR no returned location is a confident match:**
|
* If there is a similar location in listLocation, you should use this tool. It doesnt have to be an exact match.
|
||||||
* Call upsertLocation providing *only* the newly extracted InputName (required) and any other extracted details (InputAddress, etc.). **Do NOT provide a locationId in this case.** This will create a *new* location entry.
|
* Lastly, if the user asked a specific question about a location. You must do all the actions but also always use the reply tool to answer the user.
|
||||||
|
* This is the only way you can communicate with the user if they asked a query.
|
||||||
|
|
||||||
4. **Finalize:** After successfully calling upsertLocation (or determining no action could be taken), use stopAgent.
|
You should repeat the action loop until all locations on the image are done.
|
||||||
|
Once you are done, use stopAgent.
|
||||||
Tool Usage:
|
|
||||||
|
|
||||||
* **listLocations**: Searches the saved locations list based on provided criteria (like name or address). Used specifically to check if a location potentially already exists before using upsertLocation. Returns a list of matching locations, *each including its locationId*.
|
|
||||||
* **upsertLocation**: Creates or updates a location in the saved list. Requires name. Can include address, etc.
|
|
||||||
* **To UPDATE:** If you identified an existing location using listLocations, provide its locationId along with any new/updated details (name, address, etc.).
|
|
||||||
* **To CREATE:** If no existing location was found (or you are creating intentionally), provide the location details (name, address, etc.) but **omit the locationId**.
|
|
||||||
* **stopAgent**: Signals the end of the agent's processing for the current turn. Call this *after* completing the location task (create/update/failed extraction).
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const replyTool = `
|
const replyTool = `
|
||||||
@ -64,6 +67,23 @@ const replyTool = `
|
|||||||
|
|
||||||
const locationTools = `
|
const locationTools = `
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "think",
|
||||||
|
"description": "Use this tool to think through the image, evaluating the location and whether or not it exists in the users listLocations. You should also ask yourself if the user has asked a query, and if you've used the correct tool to reply to them.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"thought": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A singular thought about the image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["thought"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -79,27 +99,40 @@ const locationTools = `
|
|||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "upsertLocation",
|
"name": "createLocation",
|
||||||
"description": "Upserts a location. This is used for both creating new locations, and updating existing ones. Providing locationId from an existing ID from listLocations, will make this an update function. Not providing one will create a new location. You must provide a locationId if you think the input is a location that already exists.",
|
"description": "Creates a new location with as much information as you can extract. Be precise. You should only add the parameters you can actually see on the image.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The primary name of the location (e.g., 'Eiffel Tower', 'Mom's House', 'Acme Corp HQ'). This field is mandatory."
|
"description": "The primary name of the location"
|
||||||
},
|
},
|
||||||
"locationId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The UUID of the location. You should only provide this IF you believe the location already exists, from listLocation."
|
|
||||||
},
|
|
||||||
"address": {
|
"address": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The full street address of the location, if available (e.g., 'Champ de Mars, 5 Av. Anatole France, 75007 Paris, France'). Include if extracted."
|
"description": "The address of the location"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"]
|
"required": ["name"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "createExistingLocation",
|
||||||
|
"description": "Called when a location already exists in the users list, from listLocations. Only call this to indicate this image contains a duplicate. And only after using the doesLocationExist tol",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"locationId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The UUID of the location, from listLocations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["locationId"]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
%s
|
%s
|
||||||
{
|
{
|
||||||
@ -125,10 +158,12 @@ func getLocationAgentTools(allowReply bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type listLocationArguments struct{}
|
type listLocationArguments struct{}
|
||||||
type upsertLocationArguments struct {
|
type createLocationArguments struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
LocationID *string `json:"locationId"`
|
Address *string `json:"address"`
|
||||||
Address *string `json:"address"`
|
}
|
||||||
|
type createExistingLocationArguments struct {
|
||||||
|
LocationID string `json:"locationId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
|
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
|
||||||
@ -151,8 +186,8 @@ func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) clien
|
|||||||
return locationModel.List(context.Background(), info.UserId)
|
return locationModel.List(context.Background(), info.UserId)
|
||||||
})
|
})
|
||||||
|
|
||||||
agentClient.ToolHandler.AddTool("upsertLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
agentClient.ToolHandler.AddTool("createLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||||
args := upsertLocationArguments{}
|
args := createLocationArguments{}
|
||||||
err := json.Unmarshal([]byte(_args), &args)
|
err := json.Unmarshal([]byte(_args), &args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Locations{}, err
|
return model.Locations{}, err
|
||||||
@ -160,18 +195,9 @@ func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) clien
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
locationId := uuid.Nil
|
// TODO: this tool could be simplier, as the model could have a SaveToImage joined with the save.
|
||||||
if args.LocationID != nil {
|
|
||||||
locationUuid, err := uuid.Parse(*args.LocationID)
|
|
||||||
if err != nil {
|
|
||||||
return model.Locations{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
locationId = locationUuid
|
|
||||||
}
|
|
||||||
|
|
||||||
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
|
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
|
||||||
ID: locationId,
|
|
||||||
Name: args.Name,
|
Name: args.Name,
|
||||||
Address: args.Address,
|
Address: args.Address,
|
||||||
})
|
})
|
||||||
@ -188,9 +214,35 @@ func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) clien
|
|||||||
return location, nil
|
return location, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("createExistingLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||||
|
args := createExistingLocationArguments{}
|
||||||
|
err := json.Unmarshal([]byte(_args), &args)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
locationId, err := uuid.Parse(args.LocationID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = locationModel.SaveToImage(ctx, info.ImageId, locationId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
return "ok", nil
|
return "ok", nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
|
return "ok", nil
|
||||||
|
})
|
||||||
|
|
||||||
return agentClient
|
return agentClient
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,14 @@ const orchestratorPrompt = `
|
|||||||
* Information about an event
|
* Information about an event
|
||||||
* Content that doesn't fit any specific category or lacks actionable information.
|
* Content that doesn't fit any specific category or lacks actionable information.
|
||||||
|
|
||||||
2. **Agent Selection - Determine ALL that apply:**
|
2. **Thinking**
|
||||||
* **contactAgent:** Is there information specifically related to a person or their contact details (e.g., business card, name/email/phone)? If YES, select contactAgent.
|
* You should use the think tool to allow you to think your way through the image.
|
||||||
* **locationAgent:** Is there information specifically identifying a place, location, or address (e.g., map, street sign, address text)? If YES, select locationAgent.
|
* You should call this as many times as you need to in order to describe and analyse the image correctly.
|
||||||
* **eventAgent:** Is there information specifically related to an event (e.g., invitation, poster with date/time, schedule)? If YES, select eventAgent.
|
|
||||||
|
3. **Agent Selection - Determine ALL that apply:**
|
||||||
|
* **contactAgent:** Is there information specifically related to a person or their contact details (e.g., business card, name/email/phone)?
|
||||||
|
* **locationAgent:** Is there information specifically identifying a place, location, city, or address (e.g., map, street sign, address text)?
|
||||||
|
* **eventAgent:** Is there information specifically related to an event (e.g., invitation, poster with date/time, schedule)?
|
||||||
* **noteAgent** Does the image contain *any* text/writing (including code, formulas)?
|
* **noteAgent** Does the image contain *any* text/writing (including code, formulas)?
|
||||||
* **noAgent**: Call this when you are done working on this image.
|
* **noAgent**: Call this when you are done working on this image.
|
||||||
|
|
||||||
@ -32,6 +36,23 @@ const orchestratorPrompt = `
|
|||||||
|
|
||||||
const orchestratorTools = `
|
const orchestratorTools = `
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "think",
|
||||||
|
"description": "Use to layout all your thoughts about the image, roughly describing it, and specially describing if the image contains anything relevant to your available agents",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"thought": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A singular thought about the image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -60,7 +81,7 @@ const orchestratorTools = `
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "locationAgent",
|
"name": "locationAgent",
|
||||||
"description": "Identifies and extracts specific geographic locations or addresses. Use for content like street addresses on mail or signs, place names (e.g., restaurant, shop), map snippets, or recognizable landmarks.",
|
"description": "Use when the input has anything to do with a place. This could be a city, an address, a postcode, a virtual meeting location, or a geographical location.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
@ -113,8 +134,12 @@ func NewOrchestratorAgent(log *log.Logger, noteAgent NoteAgent, contactAgent cli
|
|||||||
EndToolCall: "noAgent",
|
EndToolCall: "noAgent",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
agent.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
|
return "Thought", nil
|
||||||
|
})
|
||||||
|
|
||||||
agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
// go noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData)
|
go noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData)
|
||||||
|
|
||||||
return "noteAgent called successfully", nil
|
return "noteAgent called successfully", nil
|
||||||
})
|
})
|
||||||
@ -126,13 +151,13 @@ func NewOrchestratorAgent(log *log.Logger, noteAgent NoteAgent, contactAgent cli
|
|||||||
})
|
})
|
||||||
|
|
||||||
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
// go locationAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
go locationAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
||||||
|
|
||||||
return "locationAgent called successfully", nil
|
return "locationAgent called successfully", nil
|
||||||
})
|
})
|
||||||
|
|
||||||
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
// go eventAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
go eventAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
||||||
|
|
||||||
return "eventAgent called successfully", nil
|
return "eventAgent called successfully", nil
|
||||||
})
|
})
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"screenmark/screenmark/.gen/haystack/agents/model"
|
|
||||||
. "screenmark/screenmark/.gen/haystack/agents/table"
|
|
||||||
|
|
||||||
. "github.com/go-jet/jet/v2/postgres"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AgentModel struct {
|
|
||||||
dbPool *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AgentModel) SaveTool(ctx context.Context, agentId uuid.UUID, tool string) (model.Tools, error) {
|
|
||||||
insertToolStmt := Tools.
|
|
||||||
INSERT(Tools.Tool).
|
|
||||||
VALUES(Json(tool))
|
|
||||||
}
|
|
@ -13,18 +13,6 @@ import (
|
|||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createLogger(prefix string) *log.Logger {
|
|
||||||
logger := log.NewWithOptions(os.Stdout, log.Options{
|
|
||||||
ReportTimestamp: true,
|
|
||||||
TimeFormat: time.Kitchen,
|
|
||||||
Prefix: prefix,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
||||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -39,7 +27,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
|||||||
imageModel := models.NewImageModel(db)
|
imageModel := models.NewImageModel(db)
|
||||||
contactModel := models.NewContactModel(db)
|
contactModel := models.NewContactModel(db)
|
||||||
|
|
||||||
databaseEventLog := createLogger("Database Events 🤖")
|
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||||
databaseEventLog.SetLevel(log.DebugLevel)
|
databaseEventLog.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
err := listener.Listen("new_image")
|
err := listener.Listen("new_image")
|
||||||
@ -58,29 +46,26 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
noteAgent := agents.NewNoteAgent(createLogger("Notes 📝"), noteModel)
|
|
||||||
contactAgent := agents.NewContactAgent(createLogger("Contacts 👥"), contactModel)
|
|
||||||
locationAgent := agents.NewLocationAgent(createLogger("Locations 📍"), locationModel)
|
|
||||||
eventAgent := agents.NewEventAgent(createLogger("Events 📅"), eventModel, locationModel)
|
|
||||||
|
|
||||||
image, err := imageModel.GetToProcessWithData(ctx, imageId)
|
image, err := imageModel.GetToProcessWithData(ctx, imageId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
|
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splitWriter := createDbStdoutWriter(db, image.ImageID)
|
||||||
|
|
||||||
|
noteAgent := agents.NewNoteAgent(createLogger("Notes 📝", splitWriter), noteModel)
|
||||||
|
contactAgent := agents.NewContactAgent(createLogger("Contacts 👥", splitWriter), contactModel)
|
||||||
|
locationAgent := agents.NewLocationAgent(createLogger("Locations 📍", splitWriter), locationModel)
|
||||||
|
eventAgent := agents.NewEventAgent(createLogger("Events 📅", splitWriter), eventModel, locationModel)
|
||||||
|
|
||||||
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
|
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
|
||||||
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
|
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼"), noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
|
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼", splitWriter), noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
|
||||||
err = orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||||
if err != nil {
|
|
||||||
databaseEventLog.Error("Orchestrator failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = imageModel.FinishProcessing(ctx, image.ID)
|
_, err = imageModel.FinishProcessing(ctx, image.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)
|
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)
|
||||||
|
@ -20,6 +20,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // 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/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/wneessen/go-mail v0.6.2 // indirect
|
github.com/wneessen/go-mail v0.6.2 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
@ -6,6 +6,7 @@ github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpT
|
|||||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
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 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
@ -33,6 +34,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
@ -109,5 +114,6 @@ 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/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=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
149
backend/logs.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||||
|
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
. "github.com/go-jet/jet/v2/postgres"
|
||||||
|
"github.com/robert-nix/ansihtml"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
insertLogStmt := Logs.
|
||||||
|
INSERT(Logs.Log, Logs.ImageID).
|
||||||
|
VALUES(string(p), w.imageId)
|
||||||
|
|
||||||
|
_, err = insertLogStmt.Exec(w.dbPool)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *DatabaseWriter) GetImageLogs(ctx context.Context, imageId uuid.UUID) ([]string, error) {
|
||||||
|
getImageLogsStmt := Logs.
|
||||||
|
SELECT(Logs.Log).
|
||||||
|
WHERE(Logs.ImageID.EQ(UUID(imageId)))
|
||||||
|
|
||||||
|
logs := []model.Logs{}
|
||||||
|
err := getImageLogsStmt.QueryContext(ctx, w.dbPool, &logs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stringLogs := make([]string, len(logs))
|
||||||
|
for i, log := range logs {
|
||||||
|
stringLogs[i] = log.Log
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringLogs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLogHandler(logWriter *DatabaseWriter) func(r chi.Router) {
|
||||||
|
return func(r chi.Router) {
|
||||||
|
r.Get("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
stringImageId := r.PathValue("imageId")
|
||||||
|
imageId, err := uuid.Parse(stringImageId)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := logWriter.GetImageLogs(r.Context(), imageId)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
html := ""
|
||||||
|
|
||||||
|
imageTag := fmt.Sprintf(`<image src="http://localhost:3040/image/%s">`, stringImageId)
|
||||||
|
|
||||||
|
for _, log := range logs {
|
||||||
|
html += fmt.Sprintf("<div>%s</div>", string(ansihtml.ConvertToHTML([]byte(log)))+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
css := `
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic styling for code blocks often used for logs */
|
||||||
|
pre {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
|
||||||
|
fullHtml := fmt.Sprintf("<html><head><title>Logs</title>%s</head><body>%s%s</body></html>", css, imageTag, html)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(fullHtml))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -393,6 +393,12 @@ func main() {
|
|||||||
fmt.Fprint(w, string(json))
|
fmt.Fprint(w, string(json))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logWriter := DatabaseWriter{
|
||||||
|
dbPool: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/logs", createLogHandler(&logWriter))
|
||||||
|
|
||||||
log.Println("Listening and serving on port 3040.")
|
log.Println("Listening and serving on port 3040.")
|
||||||
if err := http.ListenAndServe(":3040", r); err != nil {
|
if err := http.ListenAndServe(":3040", r); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
DROP SCHEMA IF EXISTS haystack CASCADE;
|
DROP SCHEMA IF EXISTS haystack CASCADE;
|
||||||
DROP SCHEMA IF EXISTS agents CASCADE;
|
|
||||||
|
|
||||||
CREATE SCHEMA haystack;
|
CREATE SCHEMA haystack;
|
||||||
CREATE SCHEMA agents;
|
|
||||||
|
|
||||||
/** -----| Haystack |----- **/
|
|
||||||
|
|
||||||
/* -----| Enums |----- */
|
/* -----| Enums |----- */
|
||||||
|
|
||||||
@ -150,6 +146,11 @@ CREATE TABLE haystack.user_notes (
|
|||||||
note_id UUID NOT NULL REFERENCES haystack.notes (id)
|
note_id UUID NOT NULL REFERENCES haystack.notes (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE haystack.logs (
|
||||||
|
log TEXT NOT NULL,
|
||||||
|
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||||
|
);
|
||||||
|
|
||||||
/* -----| Indexes |----- */
|
/* -----| Indexes |----- */
|
||||||
|
|
||||||
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
|
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
|
||||||
@ -185,29 +186,6 @@ ON haystack.user_images_to_process
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE PROCEDURE notify_new_processing_image_status();
|
EXECUTE PROCEDURE notify_new_processing_image_status();
|
||||||
|
|
||||||
/** -----| Agents |----- **/
|
|
||||||
|
|
||||||
/* -----| Schema tables |----- */
|
|
||||||
|
|
||||||
CREATE TABLE agents.agents (
|
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE agents.system_prompts (
|
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
prompt TEXT NOT NULL,
|
|
||||||
|
|
||||||
agent_id UUID NOT NULL REFERENCES agents.agents (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE agents.tools (
|
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
tool JSONB NOT NULL,
|
|
||||||
|
|
||||||
agent_id UUID NOT NULL REFERENCES agents.agents (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
/* -----| Test Data |----- */
|
/* -----| Test Data |----- */
|
||||||
|
|
||||||
-- Insert a user
|
-- Insert a user
|
||||||
|
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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>
|
@ -20,6 +20,7 @@
|
|||||||
"@tabler/icons-solidjs": "^3.30.0",
|
"@tabler/icons-solidjs": "^3.30.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
|
"@tauri-apps/plugin-fs": "~2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"solid-motionone": "^1.0.3",
|
"solid-motionone": "^1.0.3",
|
||||||
"solidjs-markdown": "^0.2.0",
|
"solidjs-markdown": "^0.2.0",
|
||||||
"tailwind-scrollbar-hide": "^2.0.0",
|
"tailwind-scrollbar-hide": "^2.0.0",
|
||||||
|
"tauri-plugin-sharetarget-api": "^0.1.6",
|
||||||
"valibot": "^1.0.0-rc.2"
|
"valibot": "^1.0.0-rc.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
21
frontend/src-tauri/Cargo.lock
generated
@ -1,12 +1,13 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Haystack"
|
name = "Haystack"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
|
"chrono",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"notify",
|
"notify",
|
||||||
"serde",
|
"serde",
|
||||||
@ -14,9 +15,11 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-sharetarget",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -536,8 +539,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4275,6 +4280,20 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-sharetarget"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15a6e4638b6a5492a46847fc9e994df8cfd2dbc1bacc11f15c207d6a2163c341"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-store"
|
name = "tauri-plugin-store"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -19,18 +19,26 @@ tauri-build = { version = "2.0.0-beta.12", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.0.0-beta.12", features = ["macos-private-api"] }
|
tauri = { version = "2.0.0-beta.12", features = ["macos-private-api"] }
|
||||||
tauri-plugin-opener = "2.0.0-beta.12"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri-plugin-dialog = "2.0.0-beta.12"
|
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
base64 = "0.21.7"
|
base64 = "0.21.7"
|
||||||
tokio = { version = "1.36.0", features = ["full"] }
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
tauri-plugin-store = "2.0.0-beta.12"
|
tauri-plugin-store = "2.0.0-beta.12"
|
||||||
tauri-plugin-http = "2.0.0-beta.12"
|
tauri-plugin-http = "2.0.0-beta.12"
|
||||||
|
chrono = "0.4"
|
||||||
|
tauri-plugin-log = "2"
|
||||||
|
tauri-plugin-sharetarget = "0.1.6"
|
||||||
|
tauri-plugin-fs = "2"
|
||||||
|
tauri-plugin-dialog = "2.2.1"
|
||||||
|
tauri-plugin-opener = "2.2.6"
|
||||||
|
tauri-plugin-sharetarget = "0.1.6"
|
||||||
|
|
||||||
[target."cfg(target_os = \"macos\")".dependencies]
|
[target."cfg(target_os = \"macos\")".dependencies]
|
||||||
cocoa = "0.26"
|
cocoa = "0.26"
|
||||||
|
|
||||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||||
tauri-plugin-global-shortcut = "2.0.0-beta.12"
|
tauri-plugin-global-shortcut = "2.0.0-beta.12"
|
||||||
|
|
||||||
|
[target."cfg(target_os = \"android\")".dependencies]
|
||||||
|
tauri-plugin-sharetarget = "0.1.6"
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
|
||||||
"identifier": "default",
|
|
||||||
"description": "Capability for the main window",
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": [
|
|
||||||
"core:default",
|
|
||||||
"opener:default",
|
|
||||||
"dialog:default",
|
|
||||||
"core:window:allow-start-dragging",
|
|
||||||
"global-shortcut:allow-is-registered",
|
|
||||||
"global-shortcut:allow-register",
|
|
||||||
"global-shortcut:allow-unregister",
|
|
||||||
"global-shortcut:allow-unregister-all",
|
|
||||||
"http:default",
|
|
||||||
{
|
|
||||||
"identifier": "http:default",
|
|
||||||
"allow": [
|
|
||||||
{
|
|
||||||
"url": "https://haystack.johncosta.tech"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://localhost:3040"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
16
frontend/src-tauri/capabilities/desktop.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
identifier = "Desktop"
|
||||||
|
description = "Capabilities for desktop platforms"
|
||||||
|
windows = ["main"]
|
||||||
|
platforms = ["linux", "macOS", "windows"]
|
||||||
|
|
||||||
|
permissions = [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"fs:default",
|
||||||
|
"http:default",
|
||||||
|
{ identifier = "http:default", allow = [
|
||||||
|
{ url = "https://haystack.johncosta.tech" },
|
||||||
|
{ url = "http://localhost:3040" },
|
||||||
|
{ url = "http://192.168.1.199:3040" }
|
||||||
|
] },
|
||||||
|
]
|
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",
|
||||||
|
"sharetarget: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
@ -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
@ -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
|
6
frontend/src-tauri/gen/android/app/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/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
|
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.haystack.app"
|
||||||
|
minSdk = 24
|
||||||
|
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
@ -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,41 @@
|
|||||||
|
<?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>
|
||||||
|
</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>
|
@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Hello World!"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,6 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.haystack" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Haystack</string>
|
||||||
|
<string name="main_activity_title">Haystack</string>
|
||||||
|
</resources>
|
@ -0,0 +1,6 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.haystack" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
BIN
frontend/src-tauri/gen/android/app/upload-keystore.jks
Normal file
22
frontend/src-tauri/gen/android/build.gradle.kts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.5.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean").configure {
|
||||||
|
delete("build")
|
||||||
|
}
|
||||||
|
|
23
frontend/src-tauri/gen/android/buildSrc/build.gradle.kts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
create("pluginsForCoolKids") {
|
||||||
|
id = "rust"
|
||||||
|
implementationClass = "RustPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(gradleApi())
|
||||||
|
implementation("com.android.tools.build:gradle:8.5.1")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
|||||||
|
import java.io.File
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.logging.LogLevel
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
|
||||||
|
open class BuildTask : DefaultTask() {
|
||||||
|
@Input
|
||||||
|
var rootDirRel: String? = null
|
||||||
|
@Input
|
||||||
|
var target: String? = null
|
||||||
|
@Input
|
||||||
|
var release: Boolean? = null
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun assemble() {
|
||||||
|
val executable = """bun""";
|
||||||
|
try {
|
||||||
|
runTauriCli(executable)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
runTauriCli("$executable.cmd")
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTauriCli(executable: String) {
|
||||||
|
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||||
|
val target = target ?: throw GradleException("target cannot be null")
|
||||||
|
val release = release ?: throw GradleException("release cannot be null")
|
||||||
|
val args = listOf("tauri", "android", "android-studio-script");
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
workingDir(File(project.projectDir, rootDirRel))
|
||||||
|
executable(executable)
|
||||||
|
args(args)
|
||||||
|
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||||
|
args("-vv")
|
||||||
|
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||||
|
args("-v")
|
||||||
|
}
|
||||||
|
if (release) {
|
||||||
|
args("--release")
|
||||||
|
}
|
||||||
|
args(listOf("--target", target))
|
||||||
|
}.assertNormalExitValue()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.get
|
||||||
|
|
||||||
|
const val TASK_GROUP = "rust"
|
||||||
|
|
||||||
|
open class Config {
|
||||||
|
lateinit var rootDirRel: String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RustPlugin : Plugin<Project> {
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
|
override fun apply(project: Project) = with(project) {
|
||||||
|
config = extensions.create("rust", Config::class.java)
|
||||||
|
|
||||||
|
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||||
|
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||||
|
|
||||||
|
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||||
|
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||||
|
|
||||||
|
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||||
|
|
||||||
|
extensions.configure<ApplicationExtension> {
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
flavorDimensions.add("abi")
|
||||||
|
productFlavors {
|
||||||
|
create("universal") {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters += abiList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultArchList.forEachIndexed { index, arch ->
|
||||||
|
create(arch) {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters.add(defaultAbiList[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
for (profile in listOf("debug", "release")) {
|
||||||
|
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||||
|
val buildTask = tasks.maybeCreate(
|
||||||
|
"rustBuildUniversal$profileCapitalized",
|
||||||
|
DefaultTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for all targets"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||||
|
|
||||||
|
for (targetPair in targetsList.withIndex()) {
|
||||||
|
val targetName = targetPair.value
|
||||||
|
val targetArch = archList[targetPair.index]
|
||||||
|
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||||
|
val targetBuildTask = project.tasks.maybeCreate(
|
||||||
|
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||||
|
BuildTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for $targetArch"
|
||||||
|
rootDirRel = config.rootDirRel
|
||||||
|
target = targetName
|
||||||
|
release = profile == "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTask.dependsOn(targetBuildTask)
|
||||||
|
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||||
|
targetBuildTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
frontend/src-tauri/gen/android/gradle.properties
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
# org.gradle.java.home=/usr/lib/jvm/java-21-openjdk
|
BIN
frontend/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
frontend/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Tue May 10 19:22:52 CST 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
185
frontend/src-tauri/gen/android/gradlew
vendored
Executable file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
frontend/src-tauri/gen/android/gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
3
frontend/src-tauri/gen/android/settings.gradle
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
include ':app'
|
||||||
|
|
||||||
|
apply from: 'tauri.settings.gradle'
|
@ -1,27 +1,36 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod shortcut;
|
pub mod screenshot;
|
||||||
|
pub mod shortcut;
|
||||||
mod state;
|
mod state;
|
||||||
mod utils;
|
pub mod utils;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||||
|
mod shortcut;
|
||||||
|
|
||||||
use state::new_shared_watcher_state;
|
use state::new_shared_watcher_state;
|
||||||
use window::setup_window;
|
use window::setup_window;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||||
pub fn run() {
|
pub fn desktop() {
|
||||||
let watcher_state = new_shared_watcher_state();
|
let watcher_state = new_shared_watcher_state();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_sharetarget::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
// .plugin(tauri_plugin_dialog::init())
|
||||||
|
// .plugin(tauri_plugin_opener::init())
|
||||||
.manage(watcher_state)
|
.manage(watcher_state)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::handle_selected_folder,
|
commands::handle_selected_folder,
|
||||||
shortcut::change_shortcut,
|
shortcut::change_shortcut,
|
||||||
shortcut::unregister_shortcut,
|
shortcut::unregister_shortcut,
|
||||||
shortcut::get_current_shortcut,
|
shortcut::get_current_shortcut,
|
||||||
|
shortcut::change_screenshot_shortcut,
|
||||||
|
shortcut::unregister_screenshot_shortcut,
|
||||||
|
shortcut::get_current_screenshot_shortcut,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
setup_window(app)?;
|
setup_window(app)?;
|
||||||
@ -32,3 +41,20 @@ pub fn run() {
|
|||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||||
|
pub fn android() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
|
.plugin(tauri_plugin_sharetarget::init())
|
||||||
|
.setup(|app| {
|
||||||
|
setup_window(app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running android tauri application");
|
||||||
|
}
|
||||||
|
@ -2,5 +2,9 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
haystack_lib::run()
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||||
|
haystack_lib::desktop();
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||||
|
haystack_lib::android();
|
||||||
}
|
}
|
||||||
|
46
frontend/src-tauri/src/screenshot.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||||
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
use tauri::{AppHandle, Emitter, Runtime};
|
||||||
|
|
||||||
|
/// Takes a screenshot of a selected area and returns the image data as base64
|
||||||
|
pub fn take_area_screenshot<R: Runtime>(app: &AppHandle<R>) -> Result<String, String> {
|
||||||
|
// Create a temporary file path
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
|
||||||
|
let temp_file = temp_dir.join(format!("haystack_screenshot_{}.png", timestamp));
|
||||||
|
|
||||||
|
// Use screencapture command with -i flag for interactive selection
|
||||||
|
let output = Command::new("screencapture")
|
||||||
|
.arg("-i") // interactive selection
|
||||||
|
.arg("-x") // don't play sound
|
||||||
|
.arg("-o") // don't show cursor
|
||||||
|
.arg("-r") // don't add shadow
|
||||||
|
.arg(temp_file.to_str().unwrap())
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute screencapture: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"screencapture failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the captured image
|
||||||
|
let contents =
|
||||||
|
fs::read(&temp_file).map_err(|e| format!("Failed to read screenshot file: {}", e))?;
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
let base64_string = BASE64.encode(&contents);
|
||||||
|
|
||||||
|
// Clean up the temporary file
|
||||||
|
if let Err(e) = fs::remove_file(&temp_file) {
|
||||||
|
println!("Warning: Failed to remove temporary screenshot file: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.emit("png-processed", base64_string.clone())
|
||||||
|
.map_err(|e| format!("Failed to emit event: {}", e))?;
|
||||||
|
|
||||||
|
Ok(base64_string)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::screenshot::take_area_screenshot;
|
||||||
use tauri::App;
|
use tauri::App;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@ -9,28 +10,36 @@ use tauri_plugin_global_shortcut::ShortcutState;
|
|||||||
use tauri_plugin_store::JsonValue;
|
use tauri_plugin_store::JsonValue;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
/// Name of the Tauri storage
|
/// Constants for Tauri store configuration
|
||||||
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store";
|
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store"; // Name of the persistent store
|
||||||
|
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut"; // Key for storing the toggle window shortcut
|
||||||
|
const HAYSTACK_SCREENSHOT_SHORTCUT: &str = "haystack_screenshot_shortcut"; // Key for storing the screenshot shortcut
|
||||||
|
|
||||||
/// Key for storing global shortcuts
|
/// Platform-specific default shortcuts
|
||||||
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut";
|
|
||||||
|
|
||||||
/// Default shortcut for macOS
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const DEFAULT_SHORTCUT: &str = "command+shift+k";
|
const DEFAULT_SHORTCUT: &str = "command+shift+k"; // macOS uses Command key
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const DEFAULT_SCREENSHOT_SHORTCUT: &str = "command+shift+p"; // macOS screenshot shortcut
|
||||||
|
|
||||||
/// Default shortcut for Windows and Linux
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||||
const DEFAULT_SHORTCUT: &str = "ctrl+shift+k";
|
const DEFAULT_SHORTCUT: &str = "ctrl+shift+k"; // Windows/Linux use Ctrl key
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||||
|
const DEFAULT_SCREENSHOT_SHORTCUT: &str = "ctrl+shift+p"; // Windows/Linux screenshot shortcut
|
||||||
|
|
||||||
/// Set shortcut during application startup
|
/// Initializes the global shortcut during application startup.
|
||||||
|
/// This function:
|
||||||
|
/// 1. Checks if a shortcut is already stored
|
||||||
|
/// 2. Uses the stored shortcut if it exists
|
||||||
|
/// 3. Falls back to the platform-specific default if no shortcut is stored
|
||||||
|
/// 4. Registers the shortcut with the system
|
||||||
pub fn enable_shortcut(app: &App) {
|
pub fn enable_shortcut(app: &App) {
|
||||||
|
// Get or create the persistent store
|
||||||
let store = app
|
let store = app
|
||||||
.store(HAYSTACK_TAURI_STORE)
|
.store(HAYSTACK_TAURI_STORE)
|
||||||
.expect("Creating the store should not fail");
|
.expect("Creating the store should not fail");
|
||||||
|
|
||||||
// Use stored shortcut if it exists
|
// Initialize toggle window shortcut
|
||||||
if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
|
let toggle_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
|
||||||
let stored_shortcut_str = match stored_shortcut {
|
let stored_shortcut_str = match stored_shortcut {
|
||||||
JsonValue::String(str) => str,
|
JsonValue::String(str) => str,
|
||||||
unexpected_type => panic!(
|
unexpected_type => panic!(
|
||||||
@ -38,45 +47,72 @@ pub fn enable_shortcut(app: &App) {
|
|||||||
unexpected_type
|
unexpected_type
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let stored_shortcut = stored_shortcut_str
|
stored_shortcut_str
|
||||||
.parse::<Shortcut>()
|
.parse::<Shortcut>()
|
||||||
.expect("Stored shortcut string should be valid");
|
.expect("Stored shortcut string should be valid")
|
||||||
_register_shortcut_upon_start(app, stored_shortcut); // Register stored shortcut
|
|
||||||
} else {
|
} else {
|
||||||
// Use default shortcut if none is stored
|
|
||||||
store.set(
|
store.set(
|
||||||
HAYSTACK_GLOBAL_SHORTCUT,
|
HAYSTACK_GLOBAL_SHORTCUT,
|
||||||
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
|
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
|
||||||
);
|
);
|
||||||
let default_shortcut = DEFAULT_SHORTCUT
|
DEFAULT_SHORTCUT
|
||||||
.parse::<Shortcut>()
|
.parse::<Shortcut>()
|
||||||
.expect("Default shortcut should be valid");
|
.expect("Default shortcut should be valid")
|
||||||
_register_shortcut_upon_start(app, default_shortcut); // Register default shortcut
|
};
|
||||||
}
|
|
||||||
|
// Initialize screenshot shortcut
|
||||||
|
let screenshot_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_SCREENSHOT_SHORTCUT)
|
||||||
|
{
|
||||||
|
let stored_shortcut_str = match stored_shortcut {
|
||||||
|
JsonValue::String(str) => str,
|
||||||
|
unexpected_type => panic!(
|
||||||
|
"Haystack shortcuts should be stored as strings, found type: {} ",
|
||||||
|
unexpected_type
|
||||||
|
),
|
||||||
|
};
|
||||||
|
stored_shortcut_str
|
||||||
|
.parse::<Shortcut>()
|
||||||
|
.expect("Stored shortcut string should be valid")
|
||||||
|
} else {
|
||||||
|
store.set(
|
||||||
|
HAYSTACK_SCREENSHOT_SHORTCUT,
|
||||||
|
JsonValue::String(DEFAULT_SCREENSHOT_SHORTCUT.to_string()),
|
||||||
|
);
|
||||||
|
DEFAULT_SCREENSHOT_SHORTCUT
|
||||||
|
.parse::<Shortcut>()
|
||||||
|
.expect("Default screenshot shortcut should be valid")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register both shortcuts
|
||||||
|
register_shortcut_upon_start(app, toggle_shortcut, screenshot_shortcut);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current stored shortcut as a string
|
/// Returns the currently configured shortcut as a string.
|
||||||
|
/// This is exposed as a Tauri command for the frontend to query.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
|
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
|
||||||
let shortcut = _get_shortcut(&app);
|
Ok(get_shortcut_from_store(&app))
|
||||||
Ok(shortcut)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unregister the current shortcut in Tauri
|
/// Unregisters the current global shortcut from the system.
|
||||||
|
/// This is exposed as a Tauri command for the frontend to trigger.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
|
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
|
||||||
let shortcut_str = _get_shortcut(&app);
|
let shortcut_str = get_shortcut_from_store(&app);
|
||||||
let shortcut = shortcut_str
|
let shortcut = shortcut_str
|
||||||
.parse::<Shortcut>()
|
.parse::<Shortcut>()
|
||||||
.expect("Stored shortcut string should be valid");
|
.expect("Stored shortcut string should be valid");
|
||||||
|
|
||||||
// Unregister the shortcut
|
|
||||||
app.global_shortcut()
|
app.global_shortcut()
|
||||||
.unregister(shortcut)
|
.unregister(shortcut)
|
||||||
.expect("Failed to unregister shortcut")
|
.expect("Failed to unregister shortcut")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the global shortcut
|
/// Changes the global shortcut to a new key combination.
|
||||||
|
/// This function:
|
||||||
|
/// 1. Validates the new shortcut
|
||||||
|
/// 2. Stores it in the persistent store
|
||||||
|
/// 3. Registers it with the system
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn change_shortcut<R: Runtime>(
|
pub fn change_shortcut<R: Runtime>(
|
||||||
app: AppHandle<R>,
|
app: AppHandle<R>,
|
||||||
@ -96,28 +132,40 @@ pub fn change_shortcut<R: Runtime>(
|
|||||||
store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key));
|
store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key));
|
||||||
|
|
||||||
// Register the new shortcut
|
// Register the new shortcut
|
||||||
_register_shortcut(&app, shortcut);
|
register_shortcut(&app, shortcut);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to register a shortcut, primarily for updating shortcuts
|
/// Handles the window visibility toggle logic when the shortcut is pressed.
|
||||||
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
/// This function:
|
||||||
|
/// 1. Hides the window if it's visible
|
||||||
|
/// 2. Shows and focuses the window if it's hidden
|
||||||
|
/// 3. Emits a 'focus-search' event when showing the window
|
||||||
|
fn handle_window_visibility<R: Runtime>(app: &AppHandle<R>, window: &tauri::WebviewWindow<R>) {
|
||||||
|
if window.is_visible().unwrap() {
|
||||||
|
window.hide().unwrap();
|
||||||
|
} else {
|
||||||
|
window.show().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
app.emit("focus-search", ()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a new shortcut with the system.
|
||||||
|
/// This is used when changing shortcuts during runtime.
|
||||||
|
/// The function:
|
||||||
|
/// 1. Gets the main window
|
||||||
|
/// 2. Sets up the shortcut handler
|
||||||
|
/// 3. Registers the shortcut with the system
|
||||||
|
fn register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
||||||
let main_window = app.get_webview_window("main").unwrap();
|
let main_window = app.get_webview_window("main").unwrap();
|
||||||
// Register global shortcut and define its behavior
|
|
||||||
app.global_shortcut()
|
app.global_shortcut()
|
||||||
.on_shortcut(shortcut, move |app, scut, event| {
|
.on_shortcut(shortcut, move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
// Toggle window visibility
|
handle_window_visibility(app, &main_window);
|
||||||
if main_window.is_visible().unwrap() {
|
|
||||||
main_window.hide().unwrap(); // Hide window
|
|
||||||
} else {
|
|
||||||
main_window.show().unwrap(); // Show window
|
|
||||||
main_window.set_focus().unwrap(); // Focus window
|
|
||||||
// Emit focus-search event
|
|
||||||
app.emit("focus-search", ()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -125,36 +173,49 @@ fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to register shortcuts during application startup
|
/// Registers a shortcut during application startup.
|
||||||
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
/// This is similar to register_shortcut but handles the initial plugin setup.
|
||||||
let window = app.get_webview_window("main").unwrap();
|
/// The function:
|
||||||
// Initialize global shortcut and set its handler
|
/// 1. Gets the main window
|
||||||
|
/// 2. Sets up the global shortcut plugin
|
||||||
|
/// 3. Registers the shortcut with the system
|
||||||
|
fn register_shortcut_upon_start(
|
||||||
|
app: &App,
|
||||||
|
toggle_shortcut: Shortcut,
|
||||||
|
screenshot_shortcut: Shortcut,
|
||||||
|
) {
|
||||||
|
let window = app
|
||||||
|
.get_webview_window("main")
|
||||||
|
.expect("webview to be defined");
|
||||||
|
|
||||||
app.handle()
|
app.handle()
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_global_shortcut::Builder::new()
|
tauri_plugin_global_shortcut::Builder::new()
|
||||||
.with_handler(move |app, scut, event| {
|
.with_handler(move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &toggle_shortcut {
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
// Toggle window visibility
|
handle_window_visibility(app, &window);
|
||||||
if window.is_visible().unwrap() {
|
}
|
||||||
window.hide().unwrap(); // Hide window
|
} else if scut == &screenshot_shortcut {
|
||||||
} else {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
window.show().unwrap(); // Show window
|
// TODO: Implement screenshot functionality
|
||||||
window.set_focus().unwrap(); // Focus window
|
println!("Screenshot shortcut pressed");
|
||||||
// Emit focus-search event
|
}
|
||||||
app.emit("focus-search", ()).unwrap();
|
if let Err(e) = take_area_screenshot(app) {
|
||||||
}
|
println!("Failed to take screenshot: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
app.global_shortcut().register(shortcut).unwrap(); // Register global shortcut
|
app.global_shortcut().register(toggle_shortcut).unwrap();
|
||||||
|
app.global_shortcut().register(screenshot_shortcut).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the stored global shortcut as a string
|
/// Retrieves the currently stored shortcut from the persistent store.
|
||||||
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
|
/// This is a helper function used by other functions to access the stored shortcut.
|
||||||
|
fn get_shortcut_from_store<R: Runtime>(app: &AppHandle<R>) -> String {
|
||||||
let store = app
|
let store = app
|
||||||
.get_store(HAYSTACK_TAURI_STORE)
|
.get_store(HAYSTACK_TAURI_STORE)
|
||||||
.expect("Store should already be loaded or created");
|
.expect("Store should already be loaded or created");
|
||||||
@ -170,3 +231,73 @@ pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_current_screenshot_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
|
||||||
|
Ok(get_screenshot_shortcut_from_store(&app))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn unregister_screenshot_shortcut<R: Runtime>(app: AppHandle<R>) {
|
||||||
|
let shortcut_str = get_screenshot_shortcut_from_store(&app);
|
||||||
|
let shortcut = shortcut_str
|
||||||
|
.parse::<Shortcut>()
|
||||||
|
.expect("Stored shortcut string should be valid");
|
||||||
|
|
||||||
|
app.global_shortcut()
|
||||||
|
.unregister(shortcut)
|
||||||
|
.expect("Failed to unregister screenshot shortcut")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn change_screenshot_shortcut<R: Runtime>(
|
||||||
|
app: AppHandle<R>,
|
||||||
|
_window: tauri::Window<R>,
|
||||||
|
key: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let shortcut = match key.parse::<Shortcut>() {
|
||||||
|
Ok(shortcut) => shortcut,
|
||||||
|
Err(_) => return Err(format!("Invalid screenshot shortcut {}", key)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let store = app
|
||||||
|
.get_store(HAYSTACK_TAURI_STORE)
|
||||||
|
.expect("Store should already be loaded or created");
|
||||||
|
store.set(HAYSTACK_SCREENSHOT_SHORTCUT, JsonValue::String(key));
|
||||||
|
|
||||||
|
register_screenshot_shortcut(&app, shortcut);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_screenshot_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
||||||
|
app.global_shortcut()
|
||||||
|
.on_shortcut(shortcut, move |app, scut, event| {
|
||||||
|
if scut == &shortcut {
|
||||||
|
if let ShortcutState::Pressed = event.state() {
|
||||||
|
if let Err(e) = take_area_screenshot(app) {
|
||||||
|
println!("Failed to take screenshot: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|err| format!("Failed to register new screenshot shortcut '{}'", err))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_screenshot_shortcut_from_store<R: Runtime>(app: &AppHandle<R>) -> String {
|
||||||
|
let store = app
|
||||||
|
.get_store(HAYSTACK_TAURI_STORE)
|
||||||
|
.expect("Store should already be loaded or created");
|
||||||
|
|
||||||
|
match store
|
||||||
|
.get(HAYSTACK_SCREENSHOT_SHORTCUT)
|
||||||
|
.expect("Screenshot shortcut should already be stored")
|
||||||
|
{
|
||||||
|
JsonValue::String(str) => str,
|
||||||
|
unexpected_type => panic!(
|
||||||
|
"Haystack shortcuts should be stored as strings, found type: {} ",
|
||||||
|
unexpected_type
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,24 +1,22 @@
|
|||||||
use tauri::App;
|
use tauri::App;
|
||||||
use tauri::TitleBarStyle;
|
|
||||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
pub fn setup_window(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn setup_window(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
||||||
.inner_size(480.0, 360.0)
|
//.inner_size(480.0, 360.0)
|
||||||
.title("Haystack")
|
//.title("Haystack")
|
||||||
.hidden_title(true)
|
//.resizable(false);
|
||||||
.resizable(false);
|
|
||||||
|
|
||||||
//
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent);
|
|
||||||
|
|
||||||
let window = win_builder.build().unwrap();
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
use cocoa::appkit::{NSColor, NSWindow};
|
use cocoa::appkit::{NSColor, NSWindow};
|
||||||
use cocoa::base::{id, nil};
|
use cocoa::base::{id, nil};
|
||||||
|
use tauri::TitleBarStyle;
|
||||||
|
|
||||||
|
let win_builder = win_builder
|
||||||
|
.hidden_title(true)
|
||||||
|
.title_bar_style(TitleBarStyle::Transparent);
|
||||||
|
let window = win_builder.build().unwrap();
|
||||||
|
|
||||||
let ns_window = window.ns_window().unwrap() as id;
|
let ns_window = window.ns_window().unwrap() as id;
|
||||||
unsafe {
|
unsafe {
|
||||||
@ -33,5 +31,9 @@ pub fn setup_window(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
win_builder.build().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { ProtectedRoute } from "./ProtectedRoute";
|
|||||||
import { Search } from "./Search";
|
import { Search } from "./Search";
|
||||||
import { Settings } from "./Settings";
|
import { Settings } from "./Settings";
|
||||||
import { ImageViewer } from "./components/ImageViewer";
|
import { ImageViewer } from "./components/ImageViewer";
|
||||||
|
import { ShareTarget } from "./components/share-target/ShareTarget";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -22,6 +23,7 @@ export const App = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageViewer />
|
<ImageViewer />
|
||||||
|
<ShareTarget />
|
||||||
<Router>
|
<Router>
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
|
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { createEffect } from "solid-js";
|
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
import { sendImage } from "../network";
|
import { sendImage } from "../network";
|
||||||
|
|
||||||
|
// TODO: This component should focus the window and show preview of screenshot,
|
||||||
|
// before we send it to backend, potentially we could draw and annotate
|
||||||
|
// OR we kill this and do stuff siltently
|
||||||
|
// anyhow keeping it like this for now
|
||||||
export function ImageViewer() {
|
export function ImageViewer() {
|
||||||
// const [latestImage, setLatestImage] = createSignal<string | null>(null);
|
// const [latestImage, setLatestImage] = createSignal<string | null>(null);
|
||||||
|
|
||||||
@ -11,10 +17,15 @@ export function ImageViewer() {
|
|||||||
console.log("Received processed PNG", event);
|
console.log("Received processed PNG", event);
|
||||||
const base64Data = event.payload as string;
|
const base64Data = event.payload as string;
|
||||||
|
|
||||||
|
const appWindow = getCurrentWindow();
|
||||||
|
|
||||||
|
appWindow.show();
|
||||||
|
appWindow.setFocus();
|
||||||
|
|
||||||
// setLatestImage(`data:image/png;base64,${base64Data}`);
|
// setLatestImage(`data:image/png;base64,${base64Data}`);
|
||||||
|
|
||||||
const result = await sendImage("test-image.png", base64Data);
|
const result = await sendImage("test-image.png", base64Data);
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
console.log("DBG: ", result);
|
console.log("DBG: ", result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -26,9 +37,7 @@ export function ImageViewer() {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
// return (
|
// return (
|
||||||
// <div>
|
// <div class="fixed inset-0">
|
||||||
// <FolderPicker />
|
|
||||||
|
|
||||||
// {latestImage() && (
|
// {latestImage() && (
|
||||||
// <div class="mt-4">
|
// <div class="mt-4">
|
||||||
// <h3>Latest Processed Image:</h3>
|
// <h3>Latest Processed Image:</h3>
|
||||||
|
47
frontend/src/components/share-target/ShareTarget.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
|
import { type Component, createEffect, createSignal, Show } from "solid-js";
|
||||||
|
import { listenForShareEvents } from "tauri-plugin-sharetarget-api";
|
||||||
|
import { sendImageFile } from "../../network";
|
||||||
|
|
||||||
|
export const ShareTarget: Component = () => {
|
||||||
|
const [file, setFile] = createSignal<File>();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const listener = listenForShareEvents(async (intent) => {
|
||||||
|
if (intent.stream == null) {
|
||||||
|
throw new Error(
|
||||||
|
"The shared item does not have a stream to read form. This might be an issue with the type of file that was shared.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.name == null) {
|
||||||
|
throw new Error("The shared item does not have a name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents = await readFile(intent.stream);
|
||||||
|
|
||||||
|
setFile(
|
||||||
|
new File([contents], intent.name, {
|
||||||
|
type: intent.content_type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
listener.then((l) => l.unregister());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This might be made better by just sending the file without setting it.
|
||||||
|
// And simply displaying a message or not displaying anything really.
|
||||||
|
createEffect(() => {
|
||||||
|
const f = file();
|
||||||
|
if (f == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendImageFile(f.name, f);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Show when={file()}>{(f) => <p>Name: {f().name}</p>}</Show>;
|
||||||
|
};
|
@ -1,8 +1,21 @@
|
|||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
|
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
console.log("Hello android!");
|
||||||
|
|
||||||
render(() => <App />, document.getElementById("root") as HTMLElement);
|
render(() => <App />, document.getElementById("root") as HTMLElement);
|
||||||
|
@ -19,9 +19,7 @@ type BaseRequestParams = Partial<{
|
|||||||
method: "GET" | "POST";
|
method: "GET" | "POST";
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const base = import.meta.env.DEV
|
export const base = "http://192.168.1.199:3040";
|
||||||
? "http://localhost:3040"
|
|
||||||
: "https://haystack.johncosta.tech";
|
|
||||||
|
|
||||||
const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => {
|
const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => {
|
||||||
return new Request(`${base}/${path}`, {
|
return new Request(`${base}/${path}`, {
|
||||||
@ -50,6 +48,23 @@ const sendImageResponseValidator = strictObject({
|
|||||||
Status: string(),
|
Status: string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sendImageFile = async (
|
||||||
|
imageName: string,
|
||||||
|
file: File,
|
||||||
|
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||||
|
const request = getBaseAuthorizedRequest({
|
||||||
|
path: `image/${imageName}`,
|
||||||
|
body: file,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
request.headers.set("Content-Type", "application/oclet-stream");
|
||||||
|
|
||||||
|
const res = await fetch(request).then((res) => res.json());
|
||||||
|
|
||||||
|
return parse(sendImageResponseValidator, res);
|
||||||
|
};
|
||||||
|
|
||||||
export const sendImage = async (
|
export const sendImage = async (
|
||||||
imageName: string,
|
imageName: string,
|
||||||
base64Image: string,
|
base64Image: string,
|
||||||
|
@ -25,10 +25,10 @@ export default defineConfig(async () => ({
|
|||||||
host: host || false,
|
host: host || false,
|
||||||
hmr: host
|
hmr: host
|
||||||
? {
|
? {
|
||||||
protocol: "ws",
|
protocol: "ws",
|
||||||
host,
|
host,
|
||||||
port: 1421,
|
port: 1421,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
// 3. tell vite to ignore watching `src-tauri`
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
|