Compare commits

...

91 Commits

Author SHA1 Message Date
Hazel Noack
4f44974c58 draft 2025-08-01 10:45:26 +02:00
Hazel Noack
a62b445f19 registering templates 2025-07-17 11:10:17 +02:00
Hazel Noack
13fae1c23f implemented search on youtube 2025-07-16 17:38:28 +02:00
Hazel Noack
21719a6cf7 added demo toml 2025-07-16 17:27:03 +02:00
addaade269 Merge pull request 'layed out goroutine' (#2) from diyhrt/interval_fetching into main
Reviewed-on: #2
2025-07-16 15:22:43 +00:00
Hazel Noack
6ee6c9c8d9 layed out goroutine 2025-07-16 16:59:40 +02:00
Hazel Noack
a0133e0981 edited todo 2025-07-16 16:38:47 +02:00
70c668bdf1 Merge pull request 'cli' (#1) from cli into main
Reviewed-on: #1
2025-07-16 14:35:52 +00:00
Hazel Noack
68c89de1a4 made some metadata of the program dynamic 2025-07-16 16:25:22 +02:00
Hazel Noack
0b7de76874 edited readme 2025-07-16 16:13:48 +02:00
Hazel Noack
797e115191 rewrote cache to work better 2025-07-16 14:24:42 +02:00
Hazel Noack
16a65df664 rewrote ftm to log 2025-07-16 12:37:58 +02:00
Hazel Noack
4e87c77ccb implemented start server 2025-07-16 12:31:51 +02:00
Hazel Noack
94b3f4c0f2 implemented start server 2025-07-16 12:31:47 +02:00
Hazel Noack
f574a00a8f not implemented cache 2025-07-16 12:09:33 +02:00
Hazel Noack
5bce49eed4 implemented specific help 2025-07-16 12:08:53 +02:00
Hazel Noack
51b788bb50 finished general help 2025-07-16 11:45:28 +02:00
Hazel Noack
7f43eb43e0 added arguments data structure 2025-07-16 11:27:32 +02:00
Hazel Noack
81b960c231 implemented general help 2025-07-16 11:20:58 +02:00
Hazel Noack
26f1008dc4 implemented basic program structure 2025-07-16 11:09:18 +02:00
Hazel Noack
f6558e3cd7 layed out the basics on how 2025-07-16 10:41:36 +02:00
Hazel Noack
60f5495f79 skipping file download if already downloaded 2025-07-15 17:46:34 +02:00
Hazel Noack
fbbd78ce72 added config options for website targets 2025-07-15 17:42:43 +02:00
Hazel Noack
e5c7dc6f44 changed website target 2025-07-15 16:03:07 +02:00
Hazel Noack
55112c96dc used standart library instead of external 2025-07-15 14:33:37 +02:00
Hazel Noack
3fd6ab5675 finished up caching of website icons 2025-07-15 14:04:42 +02:00
Hazel Noack
3396109e00 added file extention to cache 2025-07-15 13:58:44 +02:00
Hazel Noack
fb6afc2ffe minor improvement 2025-07-15 13:10:30 +02:00
Hazel Noack
808ce89dfc caching icons automatically 2025-07-15 13:08:23 +02:00
Hazel Noack
e627d02d08 used api key from config file with fallback to env 2025-07-15 11:44:33 +02:00
Hazel Noack
1e7bbc6e16 refactored diy hrt in proper struct 2025-07-15 11:41:36 +02:00
Hazel Noack
231ceea80a feat: documenting 2025-07-14 17:07:46 +02:00
Hazel Noack
5b95af791e feat: imporved card image style 2025-07-14 17:02:32 +02:00
Hazel Noack
4e6f47bff9 added example websites 2025-07-14 16:48:14 +02:00
Hazel Noack
ece0e31320 added proper styling for website images 2025-07-14 16:44:08 +02:00
Hazel Noack
7da58e1619 added raw version of website cards 2025-07-14 16:41:30 +02:00
Hazel Noack
39dfac77a9 feat: removed hiding stores 2025-07-14 16:37:59 +02:00
Hazel Noack
774fa4efb1 feat: added active cards 2025-07-14 16:36:48 +02:00
Hazel Noack
4347f9632c feat: added documentation to install and configure the server 2025-07-14 15:01:49 +02:00
Hazel Noack
228fed989e embedding index html 2025-07-14 14:35:54 +02:00
Hazel Noack
4eae59bd66 using embeded file system for static files 2025-07-14 14:30:59 +02:00
Hazel Noack
2cea019aca added profiles properly 2025-07-11 16:02:46 +02:00
Hazel Noack
560d37abd4 removed redundand statement 2025-07-11 15:31:34 +02:00
Hazel Noack
15df8b69f5 changed url regex to a stricter one 2025-07-11 15:14:51 +02:00
Hazel Noack
45111321b2 added scanning for config file 2025-07-11 15:08:33 +02:00
Hazel Noack
68522efb18 scanning and loading config file 2025-07-11 14:52:42 +02:00
Hazel Noack
f3ddddd3c6 scanning and loading config file 2025-07-11 14:51:42 +02:00
Hazel Noack
89d351c6e3 added deepl translations 2025-07-11 14:34:39 +02:00
Hazel Noack
ad204ee4e5 added todos 2025-07-11 13:01:10 +02:00
Hazel Noack
00223f3e34 disabled autocomplete 2025-07-11 12:42:58 +02:00
Hazel Noack
e7a9cfae69 added different search engines 2025-07-11 12:38:35 +02:00
Hazel Noack
e94199f9eb opening urls directly 2025-07-11 12:26:20 +02:00
Hazel Noack
e4274d1107 added search.js 2025-07-11 11:55:55 +02:00
Hazel Noack
c3e1d80b8f focus input element on loading 2025-07-11 11:42:15 +02:00
Hazel Noack
17a6953716 added border radius 2025-07-10 15:00:41 +02:00
Hazel Noack
e704e37683 added currency 2025-07-10 14:58:44 +02:00
Hazel Noack
08d2446329 added from stores 2025-07-10 14:55:10 +02:00
Hazel Noack
f4bd2a69e4 added in stock background 2025-07-10 14:46:36 +02:00
Hazel Noack
3c04714fec added listings 2025-07-10 14:40:26 +02:00
Hazel Noack
089b9ba218 cards with overflow 2025-07-10 14:22:24 +02:00
Hazel Noack
27adda431f specifically included otonoko pharmaceuticals 2025-07-10 14:11:34 +02:00
Hazel Noack
da4e11f654 added ships to 2025-07-10 13:49:16 +02:00
Hazel Noack
a4ec4966d2 merg 2025-07-10 13:12:43 +02:00
Hazel Noack
8ee6fd8602 added proper dynamic store cards 2025-07-10 13:09:56 +02:00
Hazel Noack
2e1d7d5564 removed wrong stuff 2025-07-10 13:05:38 +02:00
Hazel Noack
46cc8b7989 added store filter 2025-07-10 12:41:09 +02:00
amnesia
cb2895c498 added semicolons 2025-07-04 10:58:52 +02:00
amnesia
cb47786810 added periodic title change 2025-07-04 10:56:42 +02:00
amnesia
6e8611266b added animation to title 2025-07-04 10:51:04 +02:00
amnesia
6fa6b04b40 responsive hiding of titel and estrogen 2025-07-04 10:09:30 +02:00
amnesia
75c00e447a made design better for tiling window manager 2025-07-04 09:22:25 +02:00
Hazel Noack
f23e48700b diy hrt 2025-07-03 17:27:10 +02:00
Hazel Noack
bcf97a88e0 started diy store card 2025-07-03 17:18:12 +02:00
Hazel Noack
51ec608e65 loading diyhrt data in template 2025-07-03 17:07:35 +02:00
Hazel Noack
1892da9682 implemented config 2025-07-03 15:06:57 +02:00
Hazel Noack
0de632d29c using reflect to set values in the config 2025-07-03 14:53:49 +02:00
Hazel Noack
bbe7c76ae0 fully templated index.html 2025-07-03 14:38:00 +02:00
Hazel Noack
f32461649b added rendering of index 2025-07-03 14:30:49 +02:00
Hazel Noack
3010118695 added rendering of index 2025-07-03 14:29:30 +02:00
Hazel Noack
fbc1dfd85a added rendering config 2025-07-03 14:10:56 +02:00
Hazel Noack
41a20025f9 restructuring 2025-07-03 13:59:29 +02:00
Hazel Noack
3f359d0c57 fixed fetching of optional fields 2025-07-03 12:32:32 +02:00
Hazel Noack
4570ed20cd fixed fetching of optional fields 2025-07-03 12:32:19 +02:00
Hazel Noack
3a64b15fb1 ommit on empty 2025-07-03 11:52:35 +02:00
Hazel Noack
b0b4cc4eb3 do the actual request 2025-07-03 11:47:15 +02:00
Hazel Noack
a1e06ee718 added json keys to structs 2025-07-03 11:39:58 +02:00
Hazel Noack
5461bded96 added data models of diyhrtmarketplace 2025-07-03 11:23:36 +02:00
Hazel Noack
7a3c3dd2c0 setup go 2025-07-03 11:11:40 +02:00
Hazel Noack
3a6c917fec added gitignore 2025-07-03 10:53:22 +02:00
Hazel Noack
026eedfb01 remove console 2025-07-03 10:45:21 +02:00
Hazel Noack
772440ed85 added phrases 2025-07-03 10:34:54 +02:00
32 changed files with 1447 additions and 398 deletions

15
.air.toml Normal file
View File

@@ -0,0 +1,15 @@
# .air.toml
root = "."
tmp_dir = "tmp"
[build]
bin = "/tmp/new_tab"
cmd = "go build -o /tmp/new_tab main.go"
include = ["*.go", "*.html", "*.css"]
exclude = ["tmp", "vendor"]
[color]
main = "yellow"
watcher = "cyan"
build = "green"
log = "white"

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"cSpell.words": [
"diyhrt",
"transfem"
]
}

View File

@@ -1,3 +1,64 @@
# trans new tab page
https://diyhrt.market/api/
I am using the api of [diyhrt.market](https://diyhrt.market/api/) to get the current stats data of estrogen stocks.
## Installation
To install the new tab page you can use `go install`
```sh
go install gitea.elara.ws/Hazel/transfem-startpage
```
Then you can run the program `transfem-startpage`
```sh
transfem-startpage help
```
To configure this new tab page as website, you can install the firefox extension [New Tab Override](https://addons.mozilla.org/en-US/firefox/addon/new-tab-override/). Then just configure the url as `http://127.0.0.1:{port}/`. The default port should be `5500` but it will also print it out when starting the server. Make sure to check the box `Set focus to the web page instead of the address bar` in the extension settings, because the new tab page auto focuses the search bar.
## CLI
```sh
transfem-startpage {program} {...args}
```
program | args | description
---|---|---
`help` | `program:optional` | get more information on how the cli or one program works
`start` | `profile:optional` | start the webserver for a certain profile
`cache` | `action:emum(clear;clean)` | so something with the cache
## Config and Profiles
This tool works with profiles. The default profile is `default`. If you want to load another profile just write it as command line arg after the command. To write a config File you can create the files here:
- `{profile}.toml`
- `.{profile}.toml`
- `~/.config/startpage/{profile}.toml`
If you have the server installed, you can start it with any profile by just doing:
```sh
transfem-startpage {profile}
```
## development
1. Install [air](https://github.com/air-verse/air)
2. Run air
```sh
air dev
```
## TODO
- implement templating for every one of the frontend files
- implement functionality to clear and clean cache
- host this website on a demo page
- implement ctl
- implement autocomplete with a nice go backend and fast communication. Since it all runs locally nobody should have privacy concerns NEEDS TO BE ABLE TO TOGGLED OFF FOR DEMO PAGE
WRITE DOCUMENTATION

17
demo.toml Normal file
View File

@@ -0,0 +1,17 @@
[Server]
Port = 1312
[DiyHrt]
FetchIntervals = 60
[Template]
ActiveCard = "listings"
PageTitle = "TransfemStartpage demo"
HeaderPhrases = [
"GirlJuice.Inject();",
"You.Cute = true;",
"You.Gay = true;",
"Nazi.Punch();",
"Dolls.GiveGuns();",
"Firefox > Chrome"
]

16
dev.toml Normal file
View File

@@ -0,0 +1,16 @@
[Server]
Port = 1234
[Template]
ActiveCard = "websites"
[[Template.Websites]]
Name = "Transfem Startpage"
Url = "https://gitea.elara.ws/Hazel/transfem-startpage"
ImageUrl = "https://gitea.elara.ws/assets/img/logo.svg"
[[Template.Websites]]
Name = "GoLang"
Url = "https://go.dev/"
ImageUrl = "https://go.dev/images/gophers/motorcycle.svg"

View File

@@ -1,293 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="350mm"
height="250mm"
viewBox="0 0 350 250"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="bg.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:export-bgcolor="#00000000"
inkscape:zoom="0.48315491"
inkscape:cx="734.75399"
inkscape:cy="595.04724"
inkscape:window-width="1672"
inkscape:window-height="957"
inkscape:window-x="816"
inkscape:window-y="1259"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812968"
id="rect4" />
<rect
x="203.42924"
y="897.13763"
width="220.99149"
height="62.931351"
id="rect3" />
<rect
x="140.49789"
y="166.84126"
width="339.53659"
height="86.347672"
id="rect2" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1-7" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1-2" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1-9" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1-6" />
<rect
x="140.49789"
y="166.84126"
width="339.53659"
height="86.347672"
id="rect2-0" />
<rect
x="140.49789"
y="166.84126"
width="339.53659"
height="86.347672"
id="rect2-2" />
<rect
x="140.49789"
y="166.84126"
width="339.53659"
height="86.347672"
id="rect2-2-6" />
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812969"
id="rect4-4" />
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812969"
id="rect4-3" />
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812969"
id="rect4-3-2" />
<rect
x="125.8627"
y="753.71271"
width="357.09882"
height="87.811188"
id="rect1-2-4" />
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812969"
id="rect4-3-3" />
<rect
x="1105.2356"
y="300.11078"
width="161.4389"
height="53.812969"
id="rect6" />
<rect
x="140.49789"
y="166.84126"
width="339.53659"
height="86.347672"
id="rect2-2-6-7" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
transform="matrix(0.26273149,-0.03124901,0.03124901,0.26273149,9.6318685,1.010781)"
id="text1"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan7">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26223034,0.03520772,-0.03520772,0.26223034,181.67491,-73.913601)"
id="text1-3"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1-7);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan8">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.2637851,0.02053679,-0.02053679,0.2637851,9.5700315,-96.792778)"
id="text1-1"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1-2);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan9">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26279038,-0.03074987,0.03074987,0.26279038,54.519132,-87.602003)"
id="text1-1-6"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1-2-4);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan10">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26273149,-0.03124901,0.03124901,0.26273149,160.00124,-138.94545)"
id="text1-6"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1-9);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan11">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26430785,-0.01207042,0.01207042,0.26430785,123.83488,0.36913542)"
id="text1-8"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1-6);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="125.86328"
y="784.2323"
id="tspan12">Meow</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-21.357022,-13.142783)"
id="text2"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="140.49805"
y="197.36121"
id="tspan13">gock &lt;333</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26309895,-0.02798714,0.02798714,0.26309895,43.481131,24.812837)"
id="text2-3"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2-0);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="140.49805"
y="197.36121"
id="tspan14">gock &lt;333</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26094671,-0.04371676,0.04371676,0.26094671,222.97079,141.94755)"
id="text2-8"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2-2);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="140.49805"
y="197.36121"
id="tspan15">gock &lt;333</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26260965,-0.03225692,0.03225692,0.26260965,163.16927,187.98351)"
id="text2-8-2"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2-2-6);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="140.49805"
y="197.36121"
id="tspan16">gock &lt;333</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26261141,0.03224256,-0.03224256,0.26261141,-26.436727,169.44502)"
id="text2-8-2-4"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2-2-6-7);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="140.49805"
y="197.36121"
id="tspan17">gock &lt;333</tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text3"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect3);display:inline;fill:#ffffff;stroke-width:4.91339" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,20.26179,-16.976094)"
id="text4"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan18">Gay :3</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.25941349,0.05204788,-0.05204788,0.25941349,14.622996,-20.113934)"
id="text4-8"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4-4);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan19">Gay :3</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-185.03108,79.899068)"
id="text4-1"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4-3);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan20">Gay :3</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26300551,-0.02885206,0.02885206,0.26300551,-153.43986,-14.753595)"
id="text4-1-6"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4-3-2);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan21">Gay :3</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.25999838,0.04904269,-0.04904269,0.25999838,-255.80374,45.596021)"
id="text4-1-4"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4-3-3);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan22">Gay :3</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.25999838,0.04904269,-0.04904269,0.25999838,-255.80374,45.596021)"
id="text6"
style="font-size:37.7953px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect6);display:inline;fill:#ffffff;stroke-width:4.91339"><tspan
x="1105.2363"
y="330.63074"
id="tspan23">Gay :3</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,61 +0,0 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
padding: 5em;
padding-left: auto;
padding-right: auto;
background-color: pink;
display: flex;
align-items: center;
justify-content: center;
background: url("bg.svg") center center/auto repeat, linear-gradient(
to bottom, transparent, pink
);
}
.search-grid {
margin-top: -10em;
width: 40em;
height: 30em;
display: grid;
gap: 5em;
grid-template-rows: 10em 4em;
}
.search {
grid-row: 2;
grid-column: 1 / span 2;
}
.search-logo {
grid-row: 1;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: black;
}
.search-logo img {
height: 100%;
}
.grid-item {
background-color: lightblue;
padding: 1em;
border-radius: .5em;
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NewTab</title>
<link rel="stylesheet" type="text/css" href="assets/style.css">
</head>
<body>
<form class="search-grid" action="https://duckduckgo.com/">
<div class="search-logo">
<img als="girl_juice" src="assets/girl_juice.png" />
<h2>GirlJuice.Inject()</h2>
<img als="girl_juice" src="assets/girl_juice.png" />
</div>
<input name="q" type="text" class="grid-item" class="search" placeholder="Search DuckDuckGo :3 :3 :3" />
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const marqueeElement = document.body;
let position = 0;
const speed = 1; // Adjust speed here (lower is slower)
function animateMarquee() {
position -= speed;
// Reset position when the image has scrolled completely
if (Math.abs(position) >= marqueeElement.offsetWidth) {
position = 0;
}
marqueeElement.style.backgroundPosition = `${position}px 0`;
requestAnimationFrame(animateMarquee);
}
// Start the animation
animateMarquee();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,116 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ .PageTitle }}</title>
<link rel="stylesheet" type="text/css" href="assets/style.css">
</head>
<body>
<form id="search-form" class="search-grid" action="{{ .SearchFormAction }}">
<div class="search-logo">
<img als="girl_juice" src="assets/girl_juice.png" />
<h2 class="phrases"></h2>
<img als="girl_juice" src="assets/girl_juice.png" />
</div>
<input id="search-input" name="{{ .SearchInputName }}" type="text" class="grid-item" class="search" placeholder="{{ .SearchPlaceholder }}" autocomplete="off" />
{{ if eq .ActiveCard "stores" }}
<div class="cards" id="stores">
{{ $T := .DiyHrtTarget }}
{{range $Store := .Stores }}
<a target="{{ $T }}" href="{{ $Store.Url }}" class="card">
<h3>{{ $Store.Name }}</h3>
</a>
{{- end }}
</div>
{{ end }}
{{ if eq .ActiveCard "listings" }}
<div class="cards" id="listings">
{{ $T := .DiyHrtTarget }}
{{range $Listing := .Listings }}
<a target="{{ $T }}" href="{{ $Listing.Url }}" class="card {{ if $Listing.InStock }}in-stock{{ end }}">
<h3>{{ $Listing.ProductName }}</h3>
<p>{{ $Listing.StoreName }} - {{ $Listing.Price }} {{ $Listing.PriceCurrency }}</p>
</a>
{{- end }}
</div>
{{ end }}
{{ if eq .ActiveCard "websites" }}
<div class="cards" id="websites">
{{ $T := .WebsiteTarget }}
{{range $Website := .Websites }}
<a href="{{ $Website.Url }}" class="card" target="{{ $T }}">
<h3>{{ $Website.Name }}</h3>
<img class="card-image" src="{{ $Website.ImageUrl }}" alt="{{ $Website.Name }} picture">
</a>
{{- end }}
</div>
{{ end }}
</form>
<script>
const phrases = [
{{range $Phrase := .HeaderPhrases }}
"{{ $Phrase }}",
{{- end }}
]
function setTitle(element, s, i) {
i++
element.textContent = s.substring(0, i)
if (i >=s.length) return;
setTimeout(() => setTitle(element, s, i), 100);
}
function titleChanger(element) {
setTitle(element, phrases[Math.floor(Math.random()*phrases.length)], 0);
setTimeout(() => titleChanger(element), 10000);
}
Array.from(document.querySelectorAll(".phrases")).forEach(element => {
titleChanger(element);
})
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('input')).forEach(element => {
element.focus();
})
});
document.addEventListener('DOMContentLoaded', function() {
const marqueeElement = document.body;
let xPosition = 0;
let yPosition = 0;
const xSpeed = {{ .BackgroundScrollX }}; // Adjust speed here (lower is slower)
const ySpeed = {{ .BackgroundScrollY }};
function animateMarquee() {
xPosition -= xSpeed;
yPosition -= ySpeed;
// Reset position when the image has scrolled completely
if (Math.abs(xPosition) >= marqueeElement.offsetWidth) {
xPosition = 0;
}
if (Math.abs(yPosition) >= marqueeElement.offsetHeight) {
yPosition = 0;
}
marqueeElement.style.backgroundPosition = `${xPosition}px ${yPosition}px`;
requestAnimationFrame(animateMarquee);
}
// Start the animation
animateMarquee();
});
</script>
<script src="scripts/search.js"></script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
console.log("adding features to search...");
const form = document.getElementById("search-form");
const input = document.getElementById("search-input");
// https://stackoverflow.com/a/3809435/16804841
const expression =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
const urlRegex = new RegExp(expression);
const searchEngines = {
g: {
action: "https://www.google.com/search",
name: "q",
},
d: {
action: "https://duckduckgo.com/",
name: "q",
},
y: {
action: "https://www.youtube.com/results",
name: "search_query",
},
ya: {
action: "https://yandex.com/search/",
name: "text",
},
lure: {
action: "https://lure.sh/pkgs",
name: "q",
},
};
const translationPrefixes = ["t", "translation"];
function getDeepLUrl(s) {
const parts = s.split("-");
if (parts.length != 3) {
return undefined;
}
return `https://www.deepl.com/en/translator?/#${encodeURIComponent(
parts[0].trim()
)}/${encodeURIComponent(parts[1].trim())}/${encodeURIComponent(
parts[2].trim()
)}`;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
s = input.value;
// check if url
if (s.match(urlRegex)) {
window.open(s, "_self");
return;
}
// deepl translations
let doTranslation = false;
for (const value of translationPrefixes) {
const prefix = `!${value} `;
if (s.startsWith(prefix)) {
doTranslation = true;
s = s.slice(prefix.length); // Remove the !{key} prefix
break;
}
}
if (doTranslation) {
const url = getDeepLUrl(s);
if (url) {
window.open(url.toString(), "_self");
return;
}
}
// Check if the string starts with ! followed by a key from searchEngines
let selectedEngine = {
action: form.getAttribute("action"),
name: input.getAttribute("name"),
};
for (const [key, value] of Object.entries(searchEngines)) {
const prefix = `!${key} `;
if (s.startsWith(prefix)) {
selectedEngine = value;
s = s.slice(prefix.length); // Remove the !{key} prefix
break;
}
}
const url = new URL(selectedEngine.action);
url.searchParams.set(selectedEngine.name, s.trim());
window.open(url.toString(), "_self");
});

View File

@@ -0,0 +1,115 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
background-color: pink;
display: flex;
align-items: center;
justify-content: center;
background:
url("bg.svg") center center/auto repeat,
linear-gradient(to bottom, transparent, pink);
}
.search-grid {
width: 100%;
margin-left: 10em;
margin-right: 10em;
display: grid;
gap: 5em;
grid-template-rows: 10em 4em 17em;
}
.search {
width: 100%;
grid-row: 2;
grid-column: 1 / span 2;
}
.search-logo {
grid-row: 1;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: black;
}
@media (max-height: 300px) {
.search-grid {
grid-template-rows: 4em;
}
.search-logo {
display: none;
}
}
.cards {
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-around;
flex-wrap: wrap;
gap: 1em;
overflow: auto;
}
.card {
background-color: rgba(255, 255, 255, 0.5);
width: 10em;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
color: black;
gap: 1em;
padding: 1em;
height: 15em;
border-radius: 1em;
}
#listings .card {
background-color: rgba(255, 0, 0, 0.5);
}
#listings .in-stock {
background-color: rgba(125, 255, 125, 0.5);
}
.card h3 {
margin: 0;
}
.card-image {
height: 100%;
aspect-ratio: 1/1;
}
.search-logo img {
height: 100%;
}
.grid-item {
background-color: lightblue;
padding: 1em;
border-radius: 0.5em;
}

21
go.mod Normal file
View File

@@ -0,0 +1,21 @@
module gitea.elara.ws/Hazel/transfem-startpage
go 1.24.2
require github.com/labstack/echo/v4 v4.13.4
require (
github.com/TwiN/go-color v1.4.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/labstack/echo v3.3.10+incompatible // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
)

41
go.sum Normal file
View File

@@ -0,0 +1,41 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc=
github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

98
internal/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,98 @@
package cache
import (
"crypto/sha1"
"encoding/hex"
"errors"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"gitea.elara.ws/Hazel/transfem-startpage/internal/utils"
"github.com/labstack/echo/v4"
)
type Cache struct {
CacheDir string
Disabled bool
}
func getCacheDir() (string, error) {
baseDir, err := os.UserCacheDir()
if err != nil {
baseDir = "/tmp"
}
cacheDir := filepath.Join(baseDir, utils.Name)
err = os.MkdirAll(cacheDir, 0o755)
if err != nil {
return "", err
}
return cacheDir, nil
}
func getProfileCacheDir(profile string) (string, error) {
var profileCacheDir string
cacheDir, err := getCacheDir()
if err != nil {
return profileCacheDir, err
}
profileCacheDir = filepath.Join(cacheDir, profile)
err = os.MkdirAll(cacheDir, 0o755)
return profileCacheDir, err
}
func NewCache(profile string) Cache {
cacheDir, err := getProfileCacheDir(profile)
return Cache{
CacheDir: cacheDir,
Disabled: err != nil,
}
}
const baseCacheUrl = "cache"
func (c Cache) StartStaticServer(e *echo.Echo) error {
e.Static("/"+baseCacheUrl, c.CacheDir)
return nil
}
func hashUrl(url string) string {
h := sha1.New()
io.WriteString(h, url)
return hex.EncodeToString(h.Sum(nil))
}
func (c Cache) CacheUrl(urlString string) (string, error) {
filename := hashUrl(urlString) + filepath.Ext(urlString)
targetPath := filepath.Join(c.CacheDir, filename)
// if the file was already downloaded it doesn't need to be downloaded again
if _, err := os.Stat(targetPath); errors.Is(err, os.ErrNotExist) {
resp, err := http.Get(urlString)
if !errors.Is(err, os.ErrNotExist) {
return urlString, err
}
defer resp.Body.Close()
file, err := os.Create(targetPath)
if err != nil {
return urlString, err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return urlString, err
}
} else {
return url.JoinPath(baseCacheUrl, filename)
}
return url.JoinPath(baseCacheUrl, filename)
}

9
internal/cli/cache.go Normal file
View File

@@ -0,0 +1,9 @@
package cli
import "log"
func Cache() error {
log.Println("running cache")
log.Panicln("not implemented yet")
return nil
}

102
internal/cli/cli.go Normal file
View File

@@ -0,0 +1,102 @@
package cli
import (
"log"
"os"
"gitea.elara.ws/Hazel/transfem-startpage/internal/utils"
"github.com/TwiN/go-color"
)
type ProgramFunction func() error
type Program struct {
Name string
Function ProgramFunction
ShortDescription string
LongDescription string
Arguments []Argument
}
type Argument struct {
Name string
Type string
Required bool
Description string
}
var HelpHeader = `This is the help page of ` + utils.Name + `.
` + color.Purple + utils.BinaryName + ` {program} {...args}` + color.Reset + `
The following Programs are available:`
var Programs = []Program{
{
Name: "help",
ShortDescription: "get more information on how the cli in general or a specific program works",
LongDescription: "What did you expect to find here?",
Arguments: []Argument{
{
Name: "program",
Type: "string",
Required: false,
Description: "defines the program you want to know more about",
},
},
},
{
Name: "start",
Function: Start,
ShortDescription: "start the webserver",
LongDescription: `The start program starts the webserver.
It loads the config file of the according profile.
It uses the default values if no config file was found.`,
Arguments: []Argument{
{
Name: "profile",
Type: "string",
Required: false,
Description: "tells the program which config to load, default is 'default'",
},
},
},
{
Name: "cache",
Function: Cache,
ShortDescription: "do something with the cache",
LongDescription: `Does something with the cache.
- clear: delete the whole cache
- clean: delete all files that aren't used by any program.`,
Arguments: []Argument{
{
Name: "action",
Type: "enum(clear;clean)",
Required: true,
Description: "defines what to do with the cache",
},
},
},
}
func GetProgram(programName string) Program {
for i, p := range Programs {
if p.Name == programName {
return Programs[i]
}
}
log.Panicln("couldn't find program", programName, ". EXITING")
return Program{}
}
func Cli() {
// getting around initialization cycle
Programs[0].Function = Help
programName := "help"
if len(os.Args) > 1 {
programName = os.Args[1]
}
var selectedProgram Program = GetProgram(programName)
err := selectedProgram.Function()
if err != nil {
log.Panicln(err)
}
}

90
internal/cli/help.go Normal file
View File

@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"os"
"strings"
"gitea.elara.ws/Hazel/transfem-startpage/internal/utils"
"github.com/TwiN/go-color"
)
func padString(s string, n int) string {
missing := n - len(s)
if missing <= 0 {
return s
}
for _ = range missing {
s = s + " "
}
return s
}
func getSingleArgumentString(a Argument) string {
requiredString := ""
if a.Required {
requiredString = "*"
}
return requiredString + a.Name + ":" + a.Type
}
func getArgumentString(arguments []Argument) string {
argumentString := color.Blue
for _, a := range arguments {
argumentString = argumentString + " [" + getSingleArgumentString(a) + "]"
}
return argumentString + color.Reset
}
func generalHelp() error {
fmt.Println()
fmt.Println(HelpHeader)
fmt.Println()
for _, p := range Programs {
fmt.Print(color.Bold + padString(p.Name, 7) + color.Reset)
fmt.Print(padString(getArgumentString(p.Arguments), 40) + p.ShortDescription + "\n")
}
return nil
}
func specificHelp(programName string) error {
program := GetProgram(programName)
fmt.Println(color.Bold + "MAN PAGE FOR " + strings.ToUpper(programName) + color.Reset)
fmt.Println()
fmt.Println(color.Purple + utils.BinaryName + " " + programName + color.Reset + getArgumentString(program.Arguments))
fmt.Println()
fmt.Println(color.Bold + "arguments" + color.Reset)
argumentStrings := make([]string, len(program.Arguments))
maxArgumentString := 0
for i, a := range program.Arguments {
s := getSingleArgumentString(a)
argumentStrings[i] = s
if len(s) > maxArgumentString {
maxArgumentString = len(s)
}
}
for i, a := range program.Arguments {
fmt.Println(padString(argumentStrings[i], maxArgumentString+4) + a.Description)
}
fmt.Println()
fmt.Println(program.LongDescription)
return nil
}
func Help() error {
if len(os.Args) > 2 {
return specificHelp(os.Args[2])
}
return generalHelp()
}

18
internal/cli/start.go Normal file
View File

@@ -0,0 +1,18 @@
package cli
import (
"log"
"os"
"gitea.elara.ws/Hazel/transfem-startpage/internal/server"
)
func Start() error {
profile := "default"
if len(os.Args) > 2 {
profile = os.Args[2]
}
log.Println("starting server with profile " + profile)
return server.Start(profile)
}

View File

@@ -0,0 +1,9 @@
package diyhrt
type DiyHrtConfig struct {
ApiKey string
FetchIntervals int
StoreFilter StoreFilter
ListingFilter ListingFilter
}

45
internal/diyhrt/fetch.go Normal file
View File

@@ -0,0 +1,45 @@
package diyhrt
import (
"encoding/json"
"errors"
"net/http"
"time"
)
const endpoint = "https://diyhrt.market/api/listings"
func GetListings(apiKey string) ([]Listing, error) {
if apiKey == "" {
return nil, errors.New("diyhrt API_KEY key not set. Set it as env or in DiyHrt.ApiKey")
}
// Create HTTP client
client := &http.Client{Timeout: 10 * time.Second}
// Create request
req, err := http.NewRequest("GET", endpoint+"?api_token="+apiKey, nil)
if err != nil {
return nil, err
}
// Send request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != http.StatusOK {
return nil, errors.New("unexpected status code")
}
// Decode response
var listings []Listing
if err := json.NewDecoder(resp.Body).Decode(&listings); err != nil {
return nil, err
}
return listings, nil
}

93
internal/diyhrt/filter.go Normal file
View File

@@ -0,0 +1,93 @@
package diyhrt
import (
"slices"
)
type StoreFilter struct{
Limit int
IncludeIds []int
ExcludeIds []int
ShipsTo []string
}
func (f StoreFilter) Filter (stores []Store) []Store {
result := make([]Store, 0)
if len(f.IncludeIds) > 0 {
for _, s := range stores {
if f.Limit > 0 && len(result) >= f.Limit {
break
}
if slices.Contains(f.IncludeIds, s.Id) {
result = append(result, s)
}
}
}
for _, s := range stores {
if f.Limit > 0 && len(result) >= f.Limit {
break
}
if slices.Contains(f.ExcludeIds, s.Id) || slices.Contains(f.IncludeIds, s.Id) {
continue
}
result = append(result, s)
}
return result
}
type ListingFilter struct{
Limit int
IncludeIds []int
ExcludeIds []int
FromStores []int
}
func (f ListingFilter) Filter (listings []Listing) []Listing {
result := make([]Listing, 0)
if len(f.IncludeIds) > 0 {
for _, l := range listings {
if f.Limit > 0 && len(result) >= f.Limit {
break
}
if slices.Contains(f.IncludeIds, l.Id) {
result = append(result, l)
}
}
}
for _, l := range listings {
if f.Limit > 0 && len(result) >= f.Limit {
break
}
if slices.Contains(f.ExcludeIds, l.Id) || slices.Contains(f.IncludeIds, l.Id) {
continue
}
if len(f.FromStores) > 0 && !slices.Contains(f.FromStores, l.Store.Id) {
continue
}
result = append(result, l)
}
return result
}

41
internal/diyhrt/models.go Normal file
View File

@@ -0,0 +1,41 @@
package diyhrt
type ActiveIngredient struct {
Name string `json:"name"`
Ester string `json:"ester,omitempty"`
DisplayName string `json:"display_name,omitempty"`
}
type Product struct {
Id int `json:"id,omitempty"`
Name string `json:"name"`
Image string `json:"image,omitempty"`
ActiveIngredient ActiveIngredient `json:"active_ingredient"`
}
type Store struct {
Id int `json:"id,omitempty"`
Name string `json:"name"`
Url string `json:"url"`
Description string `json:"description"`
ShipsFromCountry string `json:"ships_from_country,omitempty"`
ShipsToCountry string `json:"ships_to_country,omitempty"`
ServiceStatus string `json:"service_status"`
ServiceStatusNotes string `json:"service_status_notes,omitempty"`
PaymentMethods []string `json:"payment_methods,omitempty"`
CategoryName string `json:"category_name,omitempty"`
}
type Listing struct {
Id int `json:"id,omitempty"`
ProductName string `json:"product_name,omitempty"`
StoreName string `json:"store_name"`
Price string `json:"price,omitempty"`
PriceCurrency string `json:"price_currency,omitempty"`
State string `json:"state,omitempty"`
InStock bool `json:"in_stock"`
Url string `json:"url,omitempty"`
PricingPerUnit string `json:"pricing_per_unit,omitempty"`
Product Product `json:"product"`
Store Store `json:"store"`
}

View File

@@ -0,0 +1,142 @@
package rendering
import (
"errors"
"log"
"os"
"path/filepath"
"gitea.elara.ws/Hazel/transfem-startpage/internal/diyhrt"
"gitea.elara.ws/Hazel/transfem-startpage/internal/utils"
"github.com/pelletier/go-toml"
)
type ActiveCard string
const (
DiyHrtStores ActiveCard = "stores"
DiyHrtListings ActiveCard = "listings"
Websites ActiveCard = "websites"
)
type AnchorTarget string
const (
OpenInNewTab AnchorTarget = "_blank"
OpenInCurrentTab AnchorTarget = "_self"
OpenInParent AnchorTarget = "_parent"
OpenInTopWindow AnchorTarget = "_top"
)
type ServerConfig struct {
Port int
}
type TemplateConfig struct {
HeaderPhrases []string
BackgroundScrollX string
BackgroundScrollY string
PageTitle string
SearchPlaceholder string
SearchFormAction string
SearchInputName string
Listings []diyhrt.Listing
Stores []diyhrt.Store
DiyHrtTarget AnchorTarget
ActiveCard ActiveCard
Websites []Website
WebsiteTarget AnchorTarget
}
type Config struct {
Server ServerConfig
Template TemplateConfig
DiyHrt diyhrt.DiyHrtConfig
}
func NewConfig() Config {
return Config{
Server: ServerConfig{
Port: 5500,
},
DiyHrt: diyhrt.DiyHrtConfig{
ApiKey: os.Getenv("API_KEY"),
FetchIntervals: 60, // fetch every hour
StoreFilter: diyhrt.StoreFilter{
Limit: 0,
IncludeIds: []int{7},
},
ListingFilter: diyhrt.ListingFilter{
FromStores: []int{7},
},
},
Template: TemplateConfig{
HeaderPhrases: []string{
"GirlJuice.Inject();",
"Child.CrowdKill();",
"CopCar.Burn();",
"You.Cute = true;",
"You.Gay = true;",
"Nazi.Punch();",
"Dolls.GiveGuns();",
},
BackgroundScrollX: "1",
BackgroundScrollY: "0",
PageTitle: "TransRights",
SearchPlaceholder: "Search on DuckDuckGo",
SearchFormAction: "https://duckduckgo.com/",
SearchInputName: "q",
ActiveCard: DiyHrtListings,
DiyHrtTarget: OpenInCurrentTab,
Websites: []Website{
{Url: "https://gitea.elara.ws/Hazel/transfem-startpage", Name: "Transfem Startpage", ImageUrl: "https://gitea.elara.ws/assets/img/logo.svg"},
},
WebsiteTarget: OpenInCurrentTab,
},
}
}
func (rc *Config) ScanForConfigFile(profile string) error {
profileFile := profile + ".toml"
baseDir, cacheDirErr := os.UserConfigDir()
if cacheDirErr == nil {
configFile := filepath.Join(baseDir, utils.Name, profileFile)
if err := rc.LoadConfigFile(configFile); !errors.Is(err, os.ErrNotExist) {
return err
}
}
if err := rc.LoadConfigFile(profileFile); !errors.Is(err, os.ErrNotExist) {
return err
}
if err := rc.LoadConfigFile("." + profileFile); !errors.Is(err, os.ErrNotExist) {
return err
}
return errors.New("no config file found")
}
func (rc *Config) LoadConfigFile(file string) error {
if _, err := os.Stat(file); err != nil {
return err
}
log.Println("loading config file", file)
content, err := os.ReadFile(file)
if err != nil {
return err
}
return toml.Unmarshal(content, rc)
}

View File

@@ -0,0 +1,28 @@
package rendering
import (
"maps"
"slices"
"gitea.elara.ws/Hazel/transfem-startpage/internal/diyhrt"
)
func (c *Config) LoadDiyHrt(listings []diyhrt.Listing) {
existingStores := make(map[int]diyhrt.Store)
for _, listing := range listings {
existingStores[listing.Store.Id] = listing.Store
}
c.Template.Listings = c.DiyHrt.ListingFilter.Filter(listings)
c.Template.Stores = c.DiyHrt.StoreFilter.Filter(slices.Collect(maps.Values(existingStores)))
}
func (c *Config) FetchDiyHrt() error {
l, err := diyhrt.GetListings(c.DiyHrt.ApiKey)
if err != nil {
return err
}
c.LoadDiyHrt(l)
return nil
}

View File

@@ -0,0 +1,80 @@
package rendering
import (
"crypto/sha1"
"encoding/hex"
"errors"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
type Website struct {
Url string
Name string
ImageUrl string
IsFetched bool
}
const CacheUrl = "cache"
func GetCacheDir() (string, error) {
baseDir, err := os.UserCacheDir()
if err != nil {
return "", err
}
cacheDir := filepath.Join(baseDir, "startpage")
err = os.MkdirAll(cacheDir, 0o755)
if err != nil {
return "", err
}
return cacheDir, nil
}
func hashUrl(url string) string {
h := sha1.New()
io.WriteString(h, url)
return hex.EncodeToString(h.Sum(nil))
}
func (w *Website) Cache() error {
if w.IsFetched {
return nil
}
cacheDir, err := GetCacheDir()
if err != nil {
return err
}
filename := hashUrl(w.ImageUrl) + filepath.Ext(w.ImageUrl)
targetPath := filepath.Join(cacheDir, filename)
// if the file was already downloaded it doesn't need to be downloaded again
if _, err := os.Stat(targetPath); errors.Is(err, os.ErrNotExist) {
resp, err := http.Get(w.ImageUrl)
if err != nil {
return err
}
defer resp.Body.Close()
file, err := os.Create(targetPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
}
// set the value in the struct to the current file
w.ImageUrl, _ = url.JoinPath(CacheUrl, filename)
w.IsFetched = true
return nil
}

82
internal/server/server.go Normal file
View File

@@ -0,0 +1,82 @@
package server
import (
"log"
"net/http"
"path/filepath"
"strconv"
"time"
"gitea.elara.ws/Hazel/transfem-startpage/internal/rendering"
)
var Config = rendering.NewConfig()
func StartFetching() {
for {
log.Println("Fetch DiyHrt data...")
Config.FetchDiyHrt()
time.Sleep(time.Duration(Config.DiyHrt.FetchIntervals) * time.Second)
if Config.DiyHrt.FetchIntervals == 0 {
break
}
}
}
func GetFilepath(u string) string {
return filepath.Join("frontend", u)
}
func staticHandler(w http.ResponseWriter, r *http.Request) {
filepath := GetFilepath(r.URL.Path)
log.Println("serving file:", filepath)
http.ServeFileFS(w, r, FrontendFiles, filepath)
}
func Start(profile string) error {
err := Config.ScanForConfigFile(profile)
if err != nil {
return err
}
go StartFetching()
err = Config.FetchDiyHrt()
if err != nil {
log.Println(err)
}
http.HandleFunc("/static", staticHandler)
http.ListenAndServe(":"+strconv.Itoa(Config.Server.Port), nil)
/*
e := echo.New()
// statically serve the file
cache := cache.NewCache(profile)
if !cache.Disabled {
cache.StartStaticServer(e)
log.Println("downloading website icons...")
for i, w := range Config.Template.Websites {
u, err := cache.CacheUrl(w.ImageUrl)
if err != nil {
log.Println(err)
}
Config.Template.Websites[i].ImageUrl = u
Config.Template.Websites[i].IsFetched = true
}
}
// https://echo.labstack.com/docs/cookbook/embed-resources
staticHandler := http.FileServer(getFileSystem())
e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/", staticHandler)))
e.GET("/scripts/*", echo.WrapHandler(http.StripPrefix("/", staticHandler)))
e.GET("/", getIndex)
StartTemplating(e)
e.Logger.Fatal(e.Start(":" + strconv.Itoa(Config.Server.Port)))
*/
return nil
}

View File

@@ -0,0 +1,80 @@
package server
import (
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"path/filepath"
"text/template"
"github.com/labstack/echo/v4"
)
var FrontendFiles embed.FS
func getFileContent() string {
content, err := FrontendFiles.ReadFile("frontend/index.html")
if err != nil {
log.Fatal(err)
}
return string(content)
}
func getIndex(c echo.Context) error {
IndexTemplate := template.Must(template.New("index").Parse(getFileContent()))
var tpl bytes.Buffer
IndexTemplate.Execute(&tpl, Config.Template)
return c.HTML(http.StatusOK, tpl.String())
}
func getFileSystem() http.FileSystem {
fsys, err := fs.Sub(FrontendFiles, "frontend")
if err != nil {
panic(err)
}
return http.FS(fsys)
}
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
var t *template.Template
func ServeTemplate(c echo.Context) error {
filename := filepath.Base(c.Request().URL.Path)
if filename == "/" {
filename = "index.html"
}
fmt.Println(filename)
var tpl bytes.Buffer
t.ExecuteTemplate(&tpl, filename, Config.Template)
return c.HTML(http.StatusOK, tpl.String())
}
func StartTemplating(e *echo.Echo) {
// register templates as renderer
t = template.Must(template.ParseFS(
FrontendFiles,
"frontend/templates/*",
))
fmt.Println(t.ParseName)
e.GET("/*", ServeTemplate)
staticHandler := http.FileServer(getFileSystem())
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/", staticHandler)))
}

6
internal/utils/meta.go Normal file
View File

@@ -0,0 +1,6 @@
package utils
import "os"
var Name = "transfem-startpage"
var BinaryName = os.Args[0]

16
main.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"embed"
"gitea.elara.ws/Hazel/transfem-startpage/internal/cli"
"gitea.elara.ws/Hazel/transfem-startpage/internal/server"
)
//go:embed frontend/*
var FrontendFiles embed.FS
func main() {
server.FrontendFiles = FrontendFiles
cli.Cli()
}

1
tmp/build-errors.log Normal file
View File

@@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1