Compare commits
	
		
			88 Commits
		
	
	
		
			026eedfb01
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					5470ba1298 | ||
| 
						 | 
					13fae1c23f | ||
| 
						 | 
					21719a6cf7 | ||
| addaade269 | |||
| 
						 | 
					6ee6c9c8d9 | ||
| 
						 | 
					a0133e0981 | ||
| 70c668bdf1 | |||
| 
						 | 
					68c89de1a4 | ||
| 
						 | 
					0b7de76874 | ||
| 
						 | 
					797e115191 | ||
| 
						 | 
					16a65df664 | ||
| 
						 | 
					4e87c77ccb | ||
| 
						 | 
					94b3f4c0f2 | ||
| 
						 | 
					f574a00a8f | ||
| 
						 | 
					5bce49eed4 | ||
| 
						 | 
					51b788bb50 | ||
| 
						 | 
					7f43eb43e0 | ||
| 
						 | 
					81b960c231 | ||
| 
						 | 
					26f1008dc4 | ||
| 
						 | 
					f6558e3cd7 | ||
| 
						 | 
					60f5495f79 | ||
| 
						 | 
					fbbd78ce72 | ||
| 
						 | 
					e5c7dc6f44 | ||
| 
						 | 
					55112c96dc | ||
| 
						 | 
					3fd6ab5675 | ||
| 
						 | 
					3396109e00 | ||
| 
						 | 
					fb6afc2ffe | ||
| 
						 | 
					808ce89dfc | ||
| 
						 | 
					e627d02d08 | ||
| 
						 | 
					1e7bbc6e16 | ||
| 
						 | 
					231ceea80a | ||
| 
						 | 
					5b95af791e | ||
| 
						 | 
					4e6f47bff9 | ||
| 
						 | 
					ece0e31320 | ||
| 
						 | 
					7da58e1619 | ||
| 
						 | 
					39dfac77a9 | ||
| 
						 | 
					774fa4efb1 | ||
| 
						 | 
					4347f9632c | ||
| 
						 | 
					228fed989e | ||
| 
						 | 
					4eae59bd66 | ||
| 
						 | 
					2cea019aca | ||
| 
						 | 
					560d37abd4 | ||
| 
						 | 
					15df8b69f5 | ||
| 
						 | 
					45111321b2 | ||
| 
						 | 
					68522efb18 | ||
| 
						 | 
					f3ddddd3c6 | ||
| 
						 | 
					89d351c6e3 | ||
| 
						 | 
					ad204ee4e5 | ||
| 
						 | 
					00223f3e34 | ||
| 
						 | 
					e7a9cfae69 | ||
| 
						 | 
					e94199f9eb | ||
| 
						 | 
					e4274d1107 | ||
| 
						 | 
					c3e1d80b8f | ||
| 
						 | 
					17a6953716 | ||
| 
						 | 
					e704e37683 | ||
| 
						 | 
					08d2446329 | ||
| 
						 | 
					f4bd2a69e4 | ||
| 
						 | 
					3c04714fec | ||
| 
						 | 
					089b9ba218 | ||
| 
						 | 
					27adda431f | ||
| 
						 | 
					da4e11f654 | ||
| 
						 | 
					a4ec4966d2 | ||
| 
						 | 
					8ee6fd8602 | ||
| 
						 | 
					2e1d7d5564 | ||
| 
						 | 
					46cc8b7989 | ||
| 
						 | 
					cb2895c498 | ||
| 
						 | 
					cb47786810 | ||
| 
						 | 
					6e8611266b | ||
| 
						 | 
					6fa6b04b40 | ||
| 
						 | 
					75c00e447a | ||
| 
						 | 
					f23e48700b | ||
| 
						 | 
					bcf97a88e0 | ||
| 
						 | 
					51ec608e65 | ||
| 
						 | 
					1892da9682 | ||
| 
						 | 
					0de632d29c | ||
| 
						 | 
					bbe7c76ae0 | ||
| 
						 | 
					f32461649b | ||
| 
						 | 
					3010118695 | ||
| 
						 | 
					fbc1dfd85a | ||
| 
						 | 
					41a20025f9 | ||
| 
						 | 
					3f359d0c57 | ||
| 
						 | 
					4570ed20cd | ||
| 
						 | 
					3a64b15fb1 | ||
| 
						 | 
					b0b4cc4eb3 | ||
| 
						 | 
					a1e06ee718 | ||
| 
						 | 
					5461bded96 | ||
| 
						 | 
					7a3c3dd2c0 | ||
| 
						 | 
					3a6c917fec | 
							
								
								
									
										15
									
								
								.air.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.air.toml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					.env
 | 
				
			||||||
							
								
								
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "cSpell.words": [
 | 
				
			||||||
 | 
					        "diyhrt",
 | 
				
			||||||
 | 
					        "transfem"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								README.md
									
									
									
									
									
								
							@@ -1,3 +1,64 @@
 | 
				
			|||||||
# trans new tab page
 | 
					# 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
									
								
							
							
						
						
									
										17
									
								
								demo.toml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								dev.toml
									
									
									
									
									
										Normal 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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -6,32 +6,32 @@ body {
 | 
				
			|||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    height: 100vh;
 | 
					    height: 100vh;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    padding-left: auto;
 | 
					 | 
				
			||||||
    padding-right: auto;
 | 
					 | 
				
			||||||
    background-color: pink;
 | 
					    background-color: pink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    background: url("bg.svg") center center/auto repeat, linear-gradient(
 | 
					    background:
 | 
				
			||||||
        to bottom, transparent, pink
 | 
					        url("bg.svg") center center/auto repeat,
 | 
				
			||||||
    );
 | 
					        linear-gradient(to bottom, transparent, pink);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-grid {
 | 
					.search-grid {
 | 
				
			||||||
    margin-top: -10em;
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    width: 40em;
 | 
					    margin-left: 10em;
 | 
				
			||||||
    height: 30em;
 | 
					    margin-right: 10em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    display: grid;
 | 
					    display: grid;
 | 
				
			||||||
    gap: 5em;
 | 
					    gap: 5em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    grid-template-rows: 10em 4em;
 | 
					    grid-template-rows: 10em 4em 17em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search {
 | 
					.search {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    grid-row: 2;
 | 
					    grid-row: 2;
 | 
				
			||||||
    grid-column: 1 / span 2;
 | 
					    grid-column: 1 / span 2;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -39,19 +39,71 @@ body {
 | 
				
			|||||||
.search-logo {
 | 
					.search-logo {
 | 
				
			||||||
    grid-row: 1;
 | 
					    grid-row: 1;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: row;
 | 
					    flex-direction: row;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    justify-content:    space-between;
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    color: black;
 | 
					    color: black;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (min-height: 700px){
 | 
					.cards {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: space-around;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					    gap: 1em;
 | 
				
			||||||
 | 
					    overflow: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-height: 500px) {
 | 
				
			||||||
    .search-grid {
 | 
					    .search-grid {
 | 
				
			||||||
        margin-top: 0;
 | 
					        grid-template-rows: 4em;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .search-logo {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .cards {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.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 {
 | 
					.search-logo img {
 | 
				
			||||||
@@ -62,5 +114,5 @@ body {
 | 
				
			|||||||
    background-color: lightblue;
 | 
					    background-color: lightblue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    padding: 1em;
 | 
					    padding: 1em;
 | 
				
			||||||
    border-radius: .5em;
 | 
					    border-radius: 0.5em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,56 +1,116 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!doctype html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
<head>
 | 
					    <head>
 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					        <meta charset="UTF-8" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>NewTab</title>
 | 
					        <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <link rel="stylesheet" type="text/css" href="assets/style.css">
 | 
					            <input id="search-input" name="{{ .SearchInputName }}" type="text" class="grid-item" class="search" placeholder="{{ .SearchPlaceholder }}" autocomplete="off" />
 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <form class="search-grid" action="https://duckduckgo.com/">
 | 
					 | 
				
			||||||
        <div class="search-logo">
 | 
					 | 
				
			||||||
            <img als="girl_juice" src="assets/girl_juice.png" />
 | 
					 | 
				
			||||||
            <h2 class="phrases">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>
 | 
					            {{ if eq .ActiveCard "stores" }}
 | 
				
			||||||
        const phrases = [
 | 
					            <div class="cards" id="stores">
 | 
				
			||||||
            "GirlJuice.Inject()",
 | 
					                {{ $T := .DiyHrtTarget }}
 | 
				
			||||||
            "Child.CrowdKill()",
 | 
					                {{range $Store := .Stores }}
 | 
				
			||||||
            "CopCar.Burn()",
 | 
					                <a target="{{ $T }}" href="{{ $Store.Url }}" class="card">
 | 
				
			||||||
            "You.Cute = true",
 | 
					                    <h3>{{ $Store.Name }}</h3>
 | 
				
			||||||
            "You.Gay = true",
 | 
					                </a>
 | 
				
			||||||
            "Nazi.Punch()",
 | 
					                {{- end }}
 | 
				
			||||||
        ]
 | 
					            </div>
 | 
				
			||||||
 | 
					            {{ end }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Array.from(document.querySelectorAll(".phrases")).forEach(element => {
 | 
					            {{ if eq .ActiveCard "listings" }}
 | 
				
			||||||
            element.textContent = phrases[Math.floor(Math.random()*phrases.length)];
 | 
					            <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 }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
					            {{ if eq .ActiveCard "websites" }}
 | 
				
			||||||
            const marqueeElement = document.body;
 | 
					            <div class="cards" id="websites">
 | 
				
			||||||
            let position = 0;
 | 
					                {{ $T := .WebsiteTarget }}
 | 
				
			||||||
            const speed = 1; // Adjust speed here (lower is slower)
 | 
					                {{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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            function animateMarquee() {
 | 
					        <script>
 | 
				
			||||||
                position -= speed;
 | 
					            const phrases = [
 | 
				
			||||||
 | 
					                {{range $Phrase := .HeaderPhrases }}
 | 
				
			||||||
 | 
					                "{{ $Phrase }}",
 | 
				
			||||||
 | 
					                {{- end }}
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Reset position when the image has scrolled completely
 | 
					            function setTitle(element, s, i) {
 | 
				
			||||||
                if (Math.abs(position) >= marqueeElement.offsetWidth) {
 | 
					              i++
 | 
				
			||||||
                    position = 0;
 | 
					              element.textContent = s.substring(0, i)
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                marqueeElement.style.backgroundPosition = `${position}px 0`;
 | 
					              if (i >=s.length) return;
 | 
				
			||||||
                requestAnimationFrame(animateMarquee);
 | 
					              setTimeout(() => setTitle(element, s, i), 100);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Start the animation
 | 
					            function titleChanger(element) {
 | 
				
			||||||
            animateMarquee();
 | 
					              setTitle(element, phrases[Math.floor(Math.random()*phrases.length)], 0);
 | 
				
			||||||
        });
 | 
					              setTimeout(() => titleChanger(element), 10000);
 | 
				
			||||||
    </script>
 | 
					            }
 | 
				
			||||||
</body>
 | 
					
 | 
				
			||||||
 | 
					            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>
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										97
									
								
								frontend/scripts/search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								frontend/scripts/search.js
									
									
									
									
									
										Normal 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");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										19
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					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/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
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										37
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					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/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/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
									
								
							
							
						
						
									
										98
									
								
								internal/cache/cache.go
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										9
									
								
								internal/cli/cache.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										102
									
								
								internal/cli/cli.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										90
									
								
								internal/cli/help.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										18
									
								
								internal/cli/start.go
									
									
									
									
									
										Normal 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								internal/diyhrt/diy_config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								internal/diyhrt/diy_config.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										45
									
								
								internal/diyhrt/fetch.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										93
									
								
								internal/diyhrt/filter.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										41
									
								
								internal/diyhrt/models.go
									
									
									
									
									
										Normal 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"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								internal/rendering/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/rendering/config.go
									
									
									
									
									
										Normal 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								internal/rendering/diyhrt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								internal/rendering/diyhrt.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										80
									
								
								internal/rendering/website_cards.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								internal/rendering/website_cards.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								internal/server/embed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								internal/server/embed.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"embed"
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gitea.elara.ws/Hazel/transfem-startpage/internal/cache"
 | 
				
			||||||
 | 
						"gitea.elara.ws/Hazel/transfem-startpage/internal/rendering"
 | 
				
			||||||
 | 
						"github.com/labstack/echo/v4"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 Start(profile string) error {
 | 
				
			||||||
 | 
						err := Config.ScanForConfigFile(profile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go StartFetching()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = Config.FetchDiyHrt()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Println(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						e.Logger.Fatal(e.Start(":" + strconv.Itoa(Config.Server.Port)))
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								internal/utils/meta.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/utils/meta.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Name = "transfem-startpage"
 | 
				
			||||||
 | 
					var BinaryName = os.Args[0]
 | 
				
			||||||
							
								
								
									
										16
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								main.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								tmp/build-errors.log
									
									
									
									
									
										Normal 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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user