16 Commits

Author SHA1 Message Date
9407f54677 fix: mobile entry point cfg_attr + java sdk versions 2025-04-26 11:44:02 +01:00
2bcf06f5c6 feat(share-target): seperate component to handle sharing 2025-04-26 11:44:02 +01:00
ecd59ec814 feat: bringing back shortcuts 2025-04-26 11:44:02 +01:00
0ef20264c1 chore(platforms): seperating permissions and inits on different platforms 2025-04-26 11:43:57 +01:00
0c2c8bde74 fix: backend err 2025-04-26 11:43:37 +01:00
bb4760036e feat: sending image to the backend 2025-04-26 11:43:37 +01:00
3e8df1ba6f feat(images): share target working and receiving images! 2025-04-26 11:43:37 +01:00
1c265d8a60 feat: working android dev environment 2025-04-26 11:43:10 +01:00
4f2b78b9f1 wip: working release sdk 2025-04-26 11:43:00 +01:00
d935d6a8b9 wip: sorting out various versioning problems 2025-04-26 11:43:00 +01:00
03656cf42e BIGWIP(android): trying to make an android release
fucking stupid shit why is it so hard
2025-04-26 11:43:00 +01:00
e735aca168 refactor(image-viewer): update ImageViewer component structure and remove commented code 2025-04-22 22:13:40 +02:00
8ea0d53af7 feat(screenshot): implement area screenshot functionality and integrate with shortcut management 2025-04-22 22:04:26 +02:00
8850b00595 feat(shortcuts): add screenshot shortcut management and registration 2025-04-22 21:36:37 +02:00
e0fadb2c66 feat(shortcuts): improve global shortcut management and documentation 2025-04-22 21:00:48 +02:00
c3fc915e60 feat(image-viewer): enhance window focus and visibility handling in ImageViewer component 2025-04-22 20:56:08 +02:00
69 changed files with 2090 additions and 119 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View 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 users 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 Solids 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.

View File

@ -65,12 +65,7 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
}
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)
if err != nil {
databaseEventLog.Error("Orchestrator failed", "error", err)
return
}
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
_, err = imageModel.FinishProcessing(ctx, image.ID)
if err != nil {
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)

3
frontend/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

607
frontend/.idea/caches/deviceStreaming.xml generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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">{
&quot;associatedIndex&quot;: 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>

Binary file not shown.

View File

@ -20,6 +20,7 @@
"@tabler/icons-solidjs": "^3.30.0",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-opener": "^2",
"clsx": "^2.1.1",
@ -30,6 +31,7 @@
"solid-motionone": "^1.0.3",
"solidjs-markdown": "^0.2.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tauri-plugin-sharetarget-api": "^0.1.6",
"valibot": "^1.0.0-rc.2"
},
"devDependencies": {

View File

@ -1,12 +1,13 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "Haystack"
version = "0.1.0"
dependencies = [
"base64 0.21.7",
"chrono",
"cocoa",
"notify",
"serde",
@ -14,9 +15,11 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-http",
"tauri-plugin-opener",
"tauri-plugin-sharetarget",
"tauri-plugin-store",
"tokio",
]
@ -536,8 +539,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@ -4275,6 +4280,20 @@ dependencies = [
"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]]
name = "tauri-plugin-store"
version = "2.2.0"

View File

@ -19,18 +19,26 @@ tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies]
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_json = "1"
tauri-plugin-dialog = "2.0.0-beta.12"
notify = "6.1.1"
base64 = "0.21.7"
tokio = { version = "1.36.0", features = ["full"] }
tauri-plugin-store = "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]
cocoa = "0.26"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-global-shortcut = "2.0.0-beta.12"
[target."cfg(target_os = \"android\")".dependencies]
tauri-plugin-sharetarget = "0.1.6"

View File

@ -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"
}
]
}
]
}

View 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" }
] },
]

View 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" }
] },
]

View 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

View 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

View 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

View 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")

View 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

View File

@ -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>

View File

@ -0,0 +1,3 @@
package com.haystack.app
class MainActivity : TauriActivity()

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
<resources>
<string name="app_name">Haystack</string>
<string name="main_activity_title">Haystack</string>
</resources>

View File

@ -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>

View File

@ -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>

Binary file not shown.

View 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")
}

View 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")
}

View File

@ -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()
}
}

View File

@ -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
)
}
}
}
}
}

View 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

Binary file not shown.

View 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
View 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" "$@"

View 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

View File

@ -0,0 +1,3 @@
include ':app'
apply from: 'tauri.settings.gradle'

View File

@ -1,27 +1,36 @@
mod commands;
mod shortcut;
pub mod screenshot;
pub mod shortcut;
mod state;
mod utils;
pub mod utils;
mod window;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
mod shortcut;
use state::new_shared_watcher_state;
use window::setup_window;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
pub fn desktop() {
let watcher_state = new_shared_watcher_state();
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_sharetarget::init())
// .plugin(tauri_plugin_dialog::init())
// .plugin(tauri_plugin_opener::init())
.manage(watcher_state)
.invoke_handler(tauri::generate_handler![
commands::handle_selected_folder,
shortcut::change_shortcut,
shortcut::unregister_shortcut,
shortcut::get_current_shortcut,
shortcut::change_screenshot_shortcut,
shortcut::unregister_screenshot_shortcut,
shortcut::get_current_screenshot_shortcut,
])
.setup(|app| {
setup_window(app)?;
@ -32,3 +41,20 @@ pub fn run() {
.run(tauri::generate_context!())
.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");
}

View File

@ -2,5 +2,9 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
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();
}

View 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)
}

View File

@ -1,3 +1,4 @@
use crate::screenshot::take_area_screenshot;
use tauri::App;
use tauri::AppHandle;
use tauri::Emitter;
@ -9,28 +10,36 @@ use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt;
/// Name of the Tauri storage
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store";
/// Constants for Tauri store configuration
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
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut";
/// Default shortcut for macOS
/// Platform-specific default shortcuts
#[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"))]
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) {
// Get or create the persistent store
let store = app
.store(HAYSTACK_TAURI_STORE)
.expect("Creating the store should not fail");
// Use stored shortcut if it exists
if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
// Initialize toggle window shortcut
let toggle_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str,
unexpected_type => panic!(
@ -38,45 +47,72 @@ pub fn enable_shortcut(app: &App) {
unexpected_type
),
};
let stored_shortcut = stored_shortcut_str
stored_shortcut_str
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid");
_register_shortcut_upon_start(app, stored_shortcut); // Register stored shortcut
.expect("Stored shortcut string should be valid")
} else {
// Use default shortcut if none is stored
store.set(
HAYSTACK_GLOBAL_SHORTCUT,
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
);
let default_shortcut = DEFAULT_SHORTCUT
DEFAULT_SHORTCUT
.parse::<Shortcut>()
.expect("Default shortcut should be valid");
_register_shortcut_upon_start(app, default_shortcut); // Register default shortcut
}
.expect("Default shortcut should be valid")
};
// 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]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
let shortcut = _get_shortcut(&app);
Ok(shortcut)
Ok(get_shortcut_from_store(&app))
}
/// 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]
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
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid");
// Unregister the shortcut
app.global_shortcut()
.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]
pub fn change_shortcut<R: Runtime>(
app: AppHandle<R>,
@ -96,28 +132,40 @@ pub fn change_shortcut<R: Runtime>(
store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key));
// Register the new shortcut
_register_shortcut(&app, shortcut);
register_shortcut(&app, shortcut);
Ok(())
}
/// Helper function to register a shortcut, primarily for updating shortcuts
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
/// Handles the window visibility toggle logic when the shortcut is pressed.
/// 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();
// Register global shortcut and define its behavior
app.global_shortcut()
.on_shortcut(shortcut, move |app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
// Toggle window visibility
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();
}
handle_window_visibility(app, &main_window);
}
}
})
@ -125,39 +173,49 @@ fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
.unwrap();
}
/// Helper function to register shortcuts during application startup
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
/// Registers a shortcut during application startup.
/// This is similar to register_shortcut but handles the initial plugin setup.
/// The function:
/// 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");
// Initialize global shortcut and set its handler
app.handle()
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |app, scut, event| {
if scut == &shortcut {
if scut == &toggle_shortcut {
if let ShortcutState::Pressed = event.state() {
// Toggle window visibility
if window.is_visible().unwrap() {
window.hide().unwrap(); // Hide window
} else {
window.show().unwrap(); // Show window
window.set_focus().unwrap(); // Focus window
// Emit focus-search event
app.emit("focus-search", ()).unwrap();
handle_window_visibility(app, &window);
}
} else if scut == &screenshot_shortcut {
if let ShortcutState::Pressed = event.state() {
// TODO: Implement screenshot functionality
println!("Screenshot shortcut pressed");
}
if let Err(e) = take_area_screenshot(app) {
println!("Failed to take screenshot: {}", e);
}
}
})
.build(),
)
.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
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
/// Retrieves the currently stored shortcut from the persistent store.
/// 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
.get_store(HAYSTACK_TAURI_STORE)
.expect("Store should already be loaded or created");
@ -173,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
),
}
}

View File

@ -2,10 +2,10 @@ use tauri::App;
use tauri::{WebviewUrl, WebviewWindowBuilder};
pub fn setup_window(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.inner_size(480.0, 360.0)
.title("Haystack")
.resizable(false);
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
//.inner_size(480.0, 360.0)
//.title("Haystack")
//.resizable(false);
#[cfg(target_os = "macos")]
{
@ -31,7 +31,6 @@ pub fn setup_window(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
}
}
#[cfg(target_os = "linux")]
{
win_builder.build().unwrap();
}

View File

@ -6,6 +6,7 @@ import { ProtectedRoute } from "./ProtectedRoute";
import { Search } from "./Search";
import { Settings } from "./Settings";
import { ImageViewer } from "./components/ImageViewer";
import { ShareTarget } from "./components/share-target/ShareTarget";
export const App = () => {
createEffect(() => {
@ -22,6 +23,7 @@ export const App = () => {
return (
<>
<ImageViewer />
<ShareTarget />
<Router>
<Route path="/login" component={Login} />

View File

@ -1,7 +1,13 @@
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";
// 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() {
// const [latestImage, setLatestImage] = createSignal<string | null>(null);
@ -11,10 +17,15 @@ export function ImageViewer() {
console.log("Received processed PNG", event);
const base64Data = event.payload as string;
const appWindow = getCurrentWindow();
appWindow.show();
appWindow.setFocus();
// setLatestImage(`data:image/png;base64,${base64Data}`);
const result = await sendImage("test-image.png", base64Data);
window.location.reload();
console.log("DBG: ", result);
});
@ -26,9 +37,7 @@ export function ImageViewer() {
return null;
// return (
// <div>
// <FolderPicker />
// <div class="fixed inset-0">
// {latestImage() && (
// <div class="mt-4">
// <h3>Latest Processed Image:</h3>

View 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>;
};

View File

@ -1,8 +1,21 @@
/* @refresh reload */
import { render } from "solid-js/web";
import "./index.css";
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);

View File

@ -19,9 +19,7 @@ type BaseRequestParams = Partial<{
method: "GET" | "POST";
}>;
export const base = import.meta.env.DEV
? "http://localhost:3040"
: "https://haystack.johncosta.tech";
export const base = "http://192.168.1.199:3040";
const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => {
return new Request(`${base}/${path}`, {
@ -50,6 +48,23 @@ const sendImageResponseValidator = strictObject({
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 (
imageName: string,
base64Image: string,