initial commit
This commit is contained in:
51
.air.toml
Normal file
51
.air.toml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/webapp"
|
||||||
|
cmd = "go build -o ./tmp/webapp cmd/webapp/main.go"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["tmp", "vendor", "testdata", "build"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html", "js", "css"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
bin
|
||||||
|
tmp
|
||||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
i153LYqEDGOipN2LCCvRBCV2uwJWMH7nZzxX8RKiZwfVTGgAgXBMDVNgvEYk0YtJ
|
||||||
|
https://img.recraft.ai/eGComLVtipZEl8ej_F4VfYJPevL-xgM3-UTeMYkCazw/rs:fit:512:512:0/raw:1/plain/abs://external/images/61f73d55-de40-43d2-80d5-04fbe04d752d
|
||||||
7
Taskfile.yaml
Normal file
7
Taskfile.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: "3"
|
||||||
|
tasks:
|
||||||
|
build-cli:
|
||||||
|
cmds:
|
||||||
|
- go build -o ./build/cli ./cmd/cli
|
||||||
|
generates:
|
||||||
|
- cli
|
||||||
25
assets/css/main.css
Normal file
25
assets/css/main.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#svg-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px; /* space between grid items */
|
||||||
|
justify-content: center; /* center items horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
flex: 1 1 150px; /* grow, shrink, base width */
|
||||||
|
max-width: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item:hover {
|
||||||
|
background-color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #777777;
|
||||||
|
}
|
||||||
39
assets/js/main.js
Normal file
39
assets/js/main.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const selectedIcons = new Set();
|
||||||
|
|
||||||
|
function iconClick(iconID) {
|
||||||
|
console.log(iconID);
|
||||||
|
const element = document.getElementById(iconID);
|
||||||
|
if (selectedIcons.has(iconID)) {
|
||||||
|
console.log("delete");
|
||||||
|
selectedIcons.delete(iconID);
|
||||||
|
element.classList.remove("selected");
|
||||||
|
} else {
|
||||||
|
console.log("add");
|
||||||
|
selectedIcons.add(iconID);
|
||||||
|
element.classList.add("selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
fetch("delete-selected", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
svgs: [...selectedIcons],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log("Server response:", data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
8
assets/js/tailwind.js
Normal file
8
assets/js/tailwind.js
Normal file
File diff suppressed because one or more lines are too long
21
cmd/cli/main.go
Normal file
21
cmd/cli/main.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"nkode-icon-manager/internal"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
log.Fatal("Please provide a command")
|
||||||
|
}
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "fix":
|
||||||
|
internal.FixIconHeader()
|
||||||
|
case "download":
|
||||||
|
internal.DownloadIconCli()
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown command: %s", os.Args[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
147
cmd/webapp/main.go
Normal file
147
cmd/webapp/main.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"nkode-icon-manager/internal"
|
||||||
|
"nkode-icon-manager/templates"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteSelected struct {
|
||||||
|
Svgs []string `json:"svgs" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"safeHTML": func(s string) template.HTML {
|
||||||
|
return template.HTML(s)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
templ := template.New("app-templates").Funcs(funcMap)
|
||||||
|
templ = template.Must(templ.ParseFS(templates.TemplateFS, "*.html"))
|
||||||
|
router.SetHTMLTemplate(templ)
|
||||||
|
router.Static("/assets/", "./assets")
|
||||||
|
logger := log.Default()
|
||||||
|
approvePath := "/Users/donov/svgs/clean_nkode_icons/"
|
||||||
|
_ = os.MkdirAll(approvePath, 0766)
|
||||||
|
unresolvedPath := "/Users/donov/svgs/unresolved_icons/"
|
||||||
|
_ = os.MkdirAll(unresolvedPath, 0766)
|
||||||
|
rejectedPath := "/Users/donov/svgs/rejected_icons/"
|
||||||
|
_ = os.MkdirAll(rejectedPath, 0766)
|
||||||
|
currentSVG := ""
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "home.html", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/view-all-icons", func(c *gin.Context) {
|
||||||
|
data := struct {
|
||||||
|
SVGs []internal.SVGData
|
||||||
|
Path string
|
||||||
|
SVGCount int
|
||||||
|
}{
|
||||||
|
SVGs: nil,
|
||||||
|
Path: approvePath,
|
||||||
|
SVGCount: 0,
|
||||||
|
}
|
||||||
|
svgs, err := internal.GetSvgsFromPath(approvePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Println(err)
|
||||||
|
c.HTML(200, "view-all-icons.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.SVGs = svgs
|
||||||
|
data.SVGCount = len(svgs)
|
||||||
|
c.HTML(200, "view-all-icons.html", data)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/approve-icon", func(c *gin.Context) {
|
||||||
|
svg, err := os.ReadFile(currentSVG)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("unable to read current svg: ", err)
|
||||||
|
}
|
||||||
|
data := struct {
|
||||||
|
SVG string
|
||||||
|
}{
|
||||||
|
SVG: string(svg),
|
||||||
|
}
|
||||||
|
c.HTML(200, "approve-icon.html", data)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/create-icon", func(c *gin.Context) {
|
||||||
|
if currentSVG != "" {
|
||||||
|
c.Redirect(302, "/approve-icon")
|
||||||
|
}
|
||||||
|
c.HTML(200, "create-icon.html", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/prompt-icon", func(c *gin.Context) {
|
||||||
|
var form struct {
|
||||||
|
Prompt string `form:"prompt" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
|
log.Println("error prompt icon: ", err)
|
||||||
|
c.String(400, "malformed form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
svgFilePath, err := internal.DownloadIcon(form.Prompt, unresolvedPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error prompt icon: ", err)
|
||||||
|
c.String(400, "download error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentSVG = svgFilePath
|
||||||
|
c.Redirect(302, "/approve-icon")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/approve-icon", func(c *gin.Context) {
|
||||||
|
var form struct {
|
||||||
|
Approved string `form:"approved" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
|
log.Println("error approve icon: ", err)
|
||||||
|
c.String(400, "malformed form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
splitPath := strings.Split(currentSVG, "/")
|
||||||
|
svgFile := splitPath[len(splitPath)-1]
|
||||||
|
splitSvgFile := strings.Split(svgFile, ".")
|
||||||
|
var movePath string
|
||||||
|
if form.Approved == "yes" {
|
||||||
|
movePath = approvePath
|
||||||
|
} else {
|
||||||
|
log.Println("approved: ", form.Approved)
|
||||||
|
movePath = rejectedPath
|
||||||
|
}
|
||||||
|
newSvgFilePath := internal.FindNoneCollisionFileName(splitSvgFile[0], "svg", movePath)
|
||||||
|
if err := os.Rename(currentSVG, newSvgFilePath); err != nil {
|
||||||
|
log.Fatalln("err moving svg: ", err)
|
||||||
|
}
|
||||||
|
currentSVG = ""
|
||||||
|
c.Redirect(302, "/create-icon")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/delete-selected", func(c *gin.Context) {
|
||||||
|
svgs := DeleteSelected{}
|
||||||
|
if err := c.ShouldBindBodyWithJSON(&svgs); err != nil {
|
||||||
|
log.Println("json body didn't bind ", err)
|
||||||
|
c.String(400, "err with json body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: this is very insecure. a real implementation needs to sanitize inputs
|
||||||
|
for _, svg := range svgs.Svgs {
|
||||||
|
log.Println("removing: ", svg)
|
||||||
|
if err := os.Remove(filepath.Join(approvePath, svg)); err != nil {
|
||||||
|
log.Println("error removing: ", svg, " with error ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Redirect(302, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run(":5155")
|
||||||
|
}
|
||||||
33
go.mod
Normal file
33
go.mod
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
module nkode-icon-manager
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.13.3 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.10.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
golang.org/x/arch v0.18.0 // 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
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
75
go.sum
Normal file
75
go.sum
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||||
|
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||||
|
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
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=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
211
internal/helper.go
Normal file
211
internal/helper.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SVGData struct {
|
||||||
|
Name string
|
||||||
|
SVG string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSvgsFromPath(path string) ([]SVGData, error) {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
svgs := make([]SVGData, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() || !strings.HasSuffix(file.Name(), ".svg") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
svgFile, err := os.ReadFile(filepath.Join(path, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
svg := SVGData{
|
||||||
|
Name: file.Name(),
|
||||||
|
SVG: string(svgFile),
|
||||||
|
}
|
||||||
|
svgs = append(svgs, svg)
|
||||||
|
}
|
||||||
|
return svgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Data []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateIcon(prompt string) (string, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
recraftBody := fmt.Sprintf(`{
|
||||||
|
"model": "recraftv2",
|
||||||
|
"prompt": "%s",
|
||||||
|
"style": "icon"
|
||||||
|
}`, prompt)
|
||||||
|
req, err := http.NewRequest("POST", "https://external.api.recraft.ai/v1/images/generations", strings.NewReader(recraftBody))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Authorization", "Bearer i153LYqEDGOipN2LCCvRBCV2uwJWMH7nZzxX8RKiZwfVTGgAgXBMDVNgvEYk0YtJ")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
// Step 2: Read the response body into memory
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var data Response
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to unmarshal json: %v", err)
|
||||||
|
}
|
||||||
|
if len(data.Data) == 0 {
|
||||||
|
return "", fmt.Errorf("no data found in response")
|
||||||
|
}
|
||||||
|
return data.Data[0].URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadFile(filepath string, url string) error {
|
||||||
|
// out, err := os.Create(filepath)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer out.Close()
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
svgData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanSvg := CleanHeader(string(svgData))
|
||||||
|
if err = os.WriteFile(filepath, []byte(cleanSvg), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
lowercase = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
digits = "0123456789"
|
||||||
|
|
||||||
|
allChars = lowercase + uppercase + digits + "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Allowed map[rune]struct{} = func() map[rune]struct{} {
|
||||||
|
allowed := make(map[rune]struct{})
|
||||||
|
for _, ch := range allChars {
|
||||||
|
allowed[ch] = struct{}{}
|
||||||
|
}
|
||||||
|
return allowed
|
||||||
|
}()
|
||||||
|
|
||||||
|
func SanatizePrompt(prompt string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
// Replace disallowed characters with '-'
|
||||||
|
for _, ch := range prompt {
|
||||||
|
if _, ok := Allowed[ch]; ok {
|
||||||
|
sb.WriteRune(ch)
|
||||||
|
} else {
|
||||||
|
sb.WriteRune('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixIconHeader() {
|
||||||
|
cliCmd := flag.NewFlagSet("fix", flag.ExitOnError)
|
||||||
|
svgPath := cliCmd.String("svg-path", "", "Path to folder with svgs")
|
||||||
|
outputPath := cliCmd.String("output-path", "", "Path to put updated svgs")
|
||||||
|
if err := cliCmd.Parse(os.Args[2:]); err != nil {
|
||||||
|
log.Fatalf("Failed to parse flags: %v", err)
|
||||||
|
}
|
||||||
|
svgs, err := GetSvgsFromPath(*svgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("fatal error reading svg dir %v", err)
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(*outputPath, 0700); err != nil {
|
||||||
|
log.Fatalf("fatal error making output dir %v", err)
|
||||||
|
}
|
||||||
|
for _, svg := range svgs {
|
||||||
|
cleanSvg := CleanHeader(svg.SVG)
|
||||||
|
if err = os.WriteFile(filepath.Join(*outputPath, svg.Name), []byte(cleanSvg), 0700); err != nil {
|
||||||
|
log.Printf("error writing %s; %v ", svg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadIconCli() {
|
||||||
|
cliCmd := flag.NewFlagSet("download", flag.ExitOnError)
|
||||||
|
outputPath := cliCmd.String("output-path", "", "path to output folder")
|
||||||
|
prompt := cliCmd.String("prompt", "", "recraft icon prompt")
|
||||||
|
if err := cliCmd.Parse(os.Args[2:]); err != nil {
|
||||||
|
log.Fatalf("Failed to parse flags: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := DownloadIcon(*prompt, *outputPath); err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadIcon(prompt string, outputPath string) (string, error) {
|
||||||
|
sanatizedPrompt := SanatizePrompt(prompt)
|
||||||
|
svgFilePath := FindNoneCollisionFileName(sanatizedPrompt, "svg", outputPath)
|
||||||
|
url, err := GenerateIcon(prompt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error generating icon: %v\n", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := DownloadFile(svgFilePath, url); err != nil {
|
||||||
|
log.Printf("error downloading %s : %v\n", url, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
log.Println("success")
|
||||||
|
return svgFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindNoneCollisionFileName(fileName, fileExt, path string) string {
|
||||||
|
|
||||||
|
filePath := filepath.Join(path, fileName+"."+fileExt)
|
||||||
|
for fileCount := 1; FileExists(filePath); fileCount += 1 {
|
||||||
|
log.Println("file exists: ", filePath)
|
||||||
|
filePath = filepath.Join(
|
||||||
|
path,
|
||||||
|
fmt.Sprintf("%s (%d).%s", fileName, fileCount, fileExt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanHeader(input string) string {
|
||||||
|
re := regexp.MustCompile(`\s*(style|width|height)="[^"]*"`)
|
||||||
|
return re.ReplaceAllString(input, "")
|
||||||
|
}
|
||||||
18
templates/approve-icon.html
Normal file
18
templates/approve-icon.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Approve Image</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css" />
|
||||||
|
<script src="/assets/js/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>{{.SVG | safeHTML}}</div>
|
||||||
|
<form action="/approve-icon" method="post">
|
||||||
|
<select name="approved">
|
||||||
|
<option value="yes">Yes</option>
|
||||||
|
<option value="no">No</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
templates/create-icon.html
Normal file
20
templates/create-icon.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE >
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Create Icons</title>
|
||||||
|
<script src="/assets/tailwind.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Create Icons</h1>
|
||||||
|
<form action="/prompt-icon" method="post">
|
||||||
|
<textarea
|
||||||
|
name="prompt"
|
||||||
|
rows="10"
|
||||||
|
cols="20"
|
||||||
|
placeholder="Icon Prompt"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<input type="submit" name="Submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
templates/embedded.go
Normal file
6
templates/embedded.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *
|
||||||
|
var TemplateFS embed.FS
|
||||||
15
templates/home.html
Normal file
15
templates/home.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Home</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css" />
|
||||||
|
<script src="/assets/js/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="pages">
|
||||||
|
<h2>Pages</h2>
|
||||||
|
<a href="view-all-icons">View Icons</a>
|
||||||
|
<a href="create-icon">Create Icons</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
templates/view-all-icons.html
Normal file
25
templates/view-all-icons.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>nKode Icon Manager</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css" />
|
||||||
|
<script src="/assets/js/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Number of SVGs: {{.SVGCount}}</h1>
|
||||||
|
<div id="svg-grid">
|
||||||
|
{{ range .SVGs }}
|
||||||
|
<div class="grid-item">
|
||||||
|
<div id="{{.Name}}" onclick="iconClick('{{.Name}}')" o>
|
||||||
|
{{.SVG | safeHTML}}
|
||||||
|
</div>
|
||||||
|
<div>{{.Name}}</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<h1>No SVGs Found</h1>
|
||||||
|
<p>no svgs in: {{.Path}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="deleteSelected()">Delete Selected</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user