feat(admin): update stylesheet

This commit is contained in:
sentriz
2023-02-21 23:11:54 +00:00
parent 16e6046e85
commit 222256cccb
44 changed files with 691 additions and 1026 deletions

View File

@@ -0,0 +1,10 @@
package adminui
import "embed"
//go:embed components.tmpl pages/*.tmpl
var TemplatesFS embed.FS
//go:generate npx tailwindcss@v3.2.4 --config tailwind.config.js --input style.css --output static/style.css --minify
//go:embed static/*
var StaticFS embed.FS

View File

@@ -0,0 +1,107 @@
{{ define "block" }}
<div class="border-b-2 border-r-2 border-gray-300/80 bg-gray-50 p-4">
<div class="inline-flex items-center gap-x-3">
<span class="text-gray-500/80 w-4">{{ component "icon" .Props.Icon }}{{ end }}</span>
<span class="text-gray-900 font-bold">{{ .Props.Name }}</span>
</div>
<hr class="bg-gray-900/30 my-1" />
{{ if .Props.Desc }}
<div class="font-medium text-gray-500 max-w-[700px]">{{ .Props.Desc }}</div>
{{ end }}
<div class="mt-3 max-w-[700px] ml-auto space-y-2 text-right">
{{ slot }}
</div>
</div>
{{ end }}
{{ define "link" }}
<a class="text-blue-500" href="{{ .Props.To }}">{{ slot }}<span class="hidden md:inline">&#8230;</span></a>
{{ end }}
{{ define "ext_link" }}
<a class="text-blue-500" href="{{ .Props.To }}" target="_blank" rel="noopener noreferrer">{{ slot }}</a>
{{ end }}
{{ define "layout_user" }}
<div class="px-4 text-gray-500 text-right whitespace-nowrap">
welcome {{ .User.Name }}
&#124;
{{ component "link" (props . "To" (path "/admin/home")) }}home{{ end }}
&#124;
{{ component "link" (props . "To" (path "/admin/logout")) }}logout{{ end }}
</div>
{{ slot }}
{{ end }}
{{ define "layout" }}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>gonic</title>
<link rel="stylesheet" href="{{ path "/admin/static/style.css" | noCache }}">
<link rel="shortcut icon" href="{{ path "/admin/static/favicon.ico" }}" type="image/x-icon">
<link rel="icon" href="{{ path "/admin/static/favicon.ico" }}" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<script src="{{ path "/admin/static/main.js" }}" defer></script>
</head>
<body class="font-mono leading-4 text-base text-gray-800">
<div class="container mx-auto min-w-min space-y-5 p-5">
<div class="border-b-2 border-gray-300">
<a href="{{ path "/admin/home" }}">
<img class="mx-auto w-[400px]" src="{{ path "/admin/static/gonic.png" }}">
</a>
</div>
{{ range $flash := .Flashes }}
{{ $colour := "bg-green-200" }}
{{ if eq $flash.Type "warning" }}{{ $colour = "bg-red-200" }}{{ end }}
<div class="p-4 shadow-sm {{ $colour }} inline-flex items-center gap-x-3 w-full">
<span class="text-gray-500/80 w-5">{{ component "icon" "circle-info" }}{{ end }}</span>
<span>{{ $flash.Message }}</span>
</div>
{{ end }}
{{ slot }}
<div class="px-5 text-right whitespace-nowrap">
<span class="text-gray-500">{{ .Version }}</span>
senan kelly, 2020
<span class="text-gray-500">&#124;</span>
{{ component "ext_link" (props . "To" "https://github.com/sentriz/gonic") }}github{{ end }}
</div>
</div>
</body>
</html>
{{ end }}
{{/* from https://github.com/FortAwesome/Font-Awesome/tree/6.x/svgs/brand */}}
{{/* TODO: see if we can dynamically render templates based on a variable instead of this */}}
{{ define "icon" }}
{{ if (eq . "lastfm") }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M225.8 367.1l-18.8-51s-30.5 34-76.2 34c-40.5 0-69.2-35.2-69.2-91.5 0-72.1 36.4-97.9 72.1-97.9 66.5 0 74.8 53.3 100.9 134.9 18.8 56.9 54 102.6 155.4 102.6 72.7 0 122-22.3 122-80.9 0-72.9-62.7-80.6-115-92.1-25.8-5.9-33.4-16.4-33.4-34 0-19.9 15.8-31.7 41.6-31.7 28.2 0 43.4 10.6 45.7 35.8l58.6-7c-4.7-52.8-41.1-74.5-100.9-74.5-52.8 0-104.4 19.9-104.4 83.9 0 39.9 19.4 65.1 68 76.8 44.9 10.6 79.8 13.8 79.8 45.7 0 21.7-21.1 30.5-61 30.5-59.2 0-83.9-31.1-97.9-73.9-32-96.8-43.6-163-161.3-163C45.7 113.8 0 168.3 0 261c0 89.1 45.7 137.2 127.9 137.2 66.2 0 97.9-31.1 97.9-31.1z"/></svg>
{{ else if (eq . "brain" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M184 0c30.9 0 56 25.1 56 56V456c0 30.9-25.1 56-56 56c-28.9 0-52.7-21.9-55.7-50.1c-5.2 1.4-10.7 2.1-16.3 2.1c-35.3 0-64-28.7-64-64c0-7.4 1.3-14.6 3.6-21.2C21.4 367.4 0 338.2 0 304c0-31.9 18.7-59.5 45.8-72.3C37.1 220.8 32 207 32 192c0-30.7 21.6-56.3 50.4-62.6C80.8 123.9 80 118 80 112c0-29.9 20.6-55.1 48.3-62.1C131.3 21.9 155.1 0 184 0zM328 0c28.9 0 52.6 21.9 55.7 49.9c27.8 7 48.3 32.1 48.3 62.1c0 6-.8 11.9-2.4 17.4c28.8 6.2 50.4 31.9 50.4 62.6c0 15-5.1 28.8-13.8 39.7C493.3 244.5 512 272.1 512 304c0 34.2-21.4 63.4-51.6 74.8c2.3 6.6 3.6 13.8 3.6 21.2c0 35.3-28.7 64-64 64c-5.6 0-11.1-.7-16.3-2.1c-3 28.2-26.8 50.1-55.7 50.1c-30.9 0-56-25.1-56-56V56c0-30.9 25.1-56 56-56z"/></svg>
{{ else if (eq . "chart-pie" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M302 240V16.6c0-9 7-16.6 16-16.6C441.7 0 542 100.3 542 224c0 9-7.6 16-16.6 16H302zM30 272C30 150.7 120.1 50.3 237 34.3c9.2-1.3 17 6.1 17 15.4V288L410.5 444.5c6.7 6.7 6.2 17.7-1.5 23.1C369.8 495.6 321.8 512 270 512C137.5 512 30 404.6 30 272zm526.4 16c9.3 0 16.6 7.8 15.4 17c-7.7 55.9-34.6 105.6-73.9 142.3c-6 5.6-15.4 5.2-21.2-.7L318 288H556.4z"/></svg>
{{ else if (eq . "brain" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M184 0c30.9 0 56 25.1 56 56V456c0 30.9-25.1 56-56 56c-28.9 0-52.7-21.9-55.7-50.1c-5.2 1.4-10.7 2.1-16.3 2.1c-35.3 0-64-28.7-64-64c0-7.4 1.3-14.6 3.6-21.2C21.4 367.4 0 338.2 0 304c0-31.9 18.7-59.5 45.8-72.3C37.1 220.8 32 207 32 192c0-30.7 21.6-56.3 50.4-62.6C80.8 123.9 80 118 80 112c0-29.9 20.6-55.1 48.3-62.1C131.3 21.9 155.1 0 184 0zM328 0c28.9 0 52.6 21.9 55.7 49.9c27.8 7 48.3 32.1 48.3 62.1c0 6-.8 11.9-2.4 17.4c28.8 6.2 50.4 31.9 50.4 62.6c0 15-5.1 28.8-13.8 39.7C493.3 244.5 512 272.1 512 304c0 34.2-21.4 63.4-51.6 74.8c2.3 6.6 3.6 13.8 3.6 21.2c0 35.3-28.7 64-64 64c-5.6 0-11.1-.7-16.3-2.1c-3 28.2-26.8 50.1-55.7 50.1c-30.9 0-56-25.1-56-56V56c0-30.9 25.1-56 56-56z"/></svg>
{{ else if (eq . "users" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z"/></svg>
{{ else if (eq . "user" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
{{ else if (eq . "folder-tree" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z"/></svg>
{{ else if (eq . "music" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M499.1 6.3c8.1 6 12.9 15.6 12.9 25.7v72V368c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V147L192 223.8V432c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V200 128c0-14.1 9.3-26.6 22.8-30.7l320-96c9.7-2.9 20.2-1.1 28.3 5z"/></svg>
{{ else if (eq . "rss" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M0 64C0 46.3 14.3 32 32 32c229.8 0 416 186.2 416 416c0 17.7-14.3 32-32 32s-32-14.3-32-32C384 253.6 226.4 96 32 96C14.3 96 0 81.7 0 64zM0 416a64 64 0 1 1 128 0A64 64 0 1 1 0 416zM32 160c159.1 0 288 128.9 288 288c0 17.7-14.3 32-32 32s-32-14.3-32-32c0-123.7-100.3-224-224-224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>
{{ else if (eq . "radio" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M494.8 47c12.7-3.7 20-17.1 16.3-29.8S494-2.8 481.2 1L51.7 126.9c-9.4 2.7-17.9 7.3-25.1 13.2C10.5 151.7 0 170.6 0 192v4V304 448c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64H218.5L494.8 47zM368 240a80 80 0 1 1 0 160 80 80 0 1 1 0-160zM80 256c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16H96c-8.8 0-16-7.2-16-16zM64 320c0-8.8 7.2-16 16-16H208c8.8 0 16 7.2 16 16s-7.2 16-16 16H80c-8.8 0-16-7.2-16-16zm16 64c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16H96c-8.8 0-16-7.2-16-16z"/></svg>
{{ else if (eq . "list" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z"/></svg>
{{ else if (eq . "circle-info" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>
{{ else if (eq . "key" ) }}
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17v80c0 13.3 10.7 24 24 24h80c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zM376 96a40 40 0 1 1 0 80 40 40 0 1 1 0-80z"/></svg>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,25 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" (printf "changing %s's avatar" .SelectedUser.Name)
) }}
<div class="flex flex-col gap-2 items-end">
{{ if ne (len .SelectedUser.Avatar) 0 }}
<img class="h-[8rem] w-[8rem] object-cover" src="data:image/jpg;base64,{{ .SelectedUser.Avatar | base64 }}" />
<form class="contents" action="{{ printf "/admin/delete_avatar_do?user=%s" .SelectedUser.Name | path }}" method="post">
<input type="submit" value="delete avatar">
</form>
{{ end }}
<form enctype="multipart/form-data" action="{{ printf "/admin/change_avatar_do?user=%s" .SelectedUser.Name | path }}" method="post">
<div class="relative pointer-events-auto">
<input class="auto-submit absolute opacity-0" name="avatar" type="file" accept="image/jpeg image/png image/gif" />
<input type="button" value="choose file">
</div>
</form>
</div>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,16 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" (printf "changing %s's password" .SelectedUser.Name)
) }}
<form class="flex flex-col gap-2 items-end" action="{{ printf "/admin/change_password_do?user=%s" .SelectedUser.Name | path }}" method="post">
<input type="password" id="password_one" name="password_one" placeholder="new password">
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
<input type="submit" value="change">
</form>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,15 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" (printf "changing %s's username" .SelectedUser.Name)
) }}
<form class="flex flex-col md:flex-row gap-2 items-end" action="{{ printf "/admin/change_username_do?user=%s" .SelectedUser.Name | path }}" method="post">
<input type="text" id="username" name="username" placeholder="new username">
<input type="submit" value="change">
</form>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,17 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" "creating new user"
) }}
<form class="flex flex-col gap-2 items-end" action="{{ path "/admin/create_user_do" }}" method="post">
<input type="text" id="username" name="username" placeholder="username">
<input type="password" id="password_one" name="password_one" placeholder="password">
<input type="password" id="password_two" name="password_two" placeholder="verify password">
<input type="submit" value="create">
</form>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,15 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" (printf "deleting user %s" .SelectedUser.Name)
"Desc" "are you sure? this will also delete their plays, playlists, starred, rated, etc."
) }}
<form class="inline-block" action="{{ printf "/admin/delete_user_do?user=%s" .SelectedUser.Name | path }}" method="post">
<input type="submit" value="yes">
</form>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,226 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "chart-pie"
"Name" "stats"
"Desc" "total items found in all watched folders"
) }}
<div class="grid grid-cols-[auto_min-content] gap-2 gap-x-5 text-right">
<div class="text-gray-500">artists</div>
<div class="font-bold">{{ .ArtistCount }}</div>
<div class="text-gray-500">albums</div>
<div class="font-bold">{{ .AlbumCount }}</div>
<div class="text-gray-500">tracks</div>
<div class="font-bold">{{ .TrackCount }}</div>
</div>
{{ end }}
{{ component "block" (props .
"Icon" "users"
"Name" "user management"
"Desc" "manage user accounts for subsonic api and web interface access"
) }}
<div class="grid grid-cols-[repeat(3,auto)_max-content] md:grid-cols-[auto_repeat(5,min-content)] gap-2 gap-x-5 items-center text-right">
{{ range $user := .AllUsers }}
<div class="col-span-3 md:col-auto ellipsis">{{ $user.Name }}</div>
<div class="text-gray-500 whitespace-nowrap">{{ $user.CreatedAt | date }}</div>
{{ component "link" (props . "To" (printf "/admin/change_username?user=%s" $user.Name | path)) }}username{{ end }}
{{ component "link" (props . "To" (printf "/admin/change_password?user=%s" $user.Name | path)) }}password{{ end }}
{{ component "link" (props . "To" (printf "/admin/change_avatar?user=%s" $user.Name | path)) }}avatar{{ end }}
{{ if $user.IsAdmin }}
<div class="text-gray-500">delete<span class="hidden md:inline">&#8230;</span></div>
{{ else }}
{{ component "link" (props . "To" (printf "/admin/delete_user?user=%s" $user.Name | path)) }}delete{{ end }}
{{ end }}
{{ end }}
{{ if .User.IsAdmin }}
<div class="col-span-full">{{ component "link" (props . "To" (path "/admin/create_user")) }}create new{{ end }}</div>
{{ end }}
</div>
{{ end }}
{{ component "block" (props .
"Icon" "folder-tree"
"Name" "recent folders"
) }}
<div class="grid grid-cols-[1fr,auto] gap-x-3 gap-y-2 items-center justify-items-end">
{{ if eq (len .RecentFolders) 0 }}
<div class="col-span-full text-gray-500">no folders yet</div>
{{ end }}
{{ range $folder := .RecentFolders }}
<div class="text-left ellipsis">{{ $folder.RightPath }}</div>
<div class="text-gray-500" title="{{ $folder.ModifiedAt }}">{{ $folder.ModifiedAt | dateHuman }}</div>
{{ end }}
{{ if and (not .IsScanning) (.User.IsAdmin) }}
{{ if not .LastScanTime.IsZero }}
<p class="col-span-full text-gray-500" title="{{ .LastScanTime }}">scanned {{ .LastScanTime | dateHuman }}</p>
{{ end }}
<form class="col-span-full" action="{{ path "/admin/start_scan_inc_do" }}" method="post">
<input type="submit" title="start a incremental scan" value="scan now">
</form>
{{ end }}
{{ if .IsScanning }}<p class="text-green-500 col-span-full">scan in progress...</p>{{ end }}
</div>
{{ end }}
{{ component "block" (props .
"Icon" "music"
"Name" "transcoding device profiles"
"Desc" "you can find your device's client name in the gonic logs. some common client names are <span class='italic text-gray-800'>DSub</span>, <span class='italic text-gray-800'>Jamstash</span>, <span class=\"italic text-gray-800\">Soundwaves</span>, or use <span class=\"italic text-gray-800\">*</span> as fallback rule for any client. see the \"transcoding profiles\" page on the wiki for more info"
) }}
<div class="grid grid-cols-[1fr_1fr_auto] gap-2 items-center justify-items-end">
{{ range $pref := .TranscodePreferences }}
{{ $formSuffix := kebabcase $pref.Client }}
<div class="ellipsis">{{ $pref.Client }}</div>
<div>{{ $pref.Profile }}</div>
<form class="contents" action="{{ printf "/admin/delete_transcode_pref_do?client=%s" $pref.Client | path }}" method="post">
<input type="submit" value="delete">
</form>
{{ end }}
<form class="contents" action="{{ path "/admin/create_transcode_pref_do" }}" method="post">
<input type="text" name="client" placeholder="client name">
<select name="profile">
{{ range $profile := .TranscodeProfiles }}<option value="{{ $profile }}">{{ $profile }}</option>{{ end }}
</select>
<input type="submit" value="save">
</form>
</div>
{{ end }}
{{ component "block" (props .
"Icon" "lastfm"
"Name" "last.fm"
"Desc" "scrobble to last.fm on a per user basis (the admin must set a global api key first)"
) }}
<div class="flex flex-col gap-2 items-end">
{{ if .CurrentLastFMAPIKey }}
{{ if .User.LastFMSession }}
<p class="text-gray-500">current status <span class="font-bold text-green-500">linked</span></p>
<form class="contents" action="{{ path "/admin/unlink_lastfm_do" }}" method="post">
<input type="submit" value="unlink">
</form>
{{ else }}
<p class="text-gray-500">current status <span class="font-bold text-red-400">unlinked</span></p>
{{ $cbPath := path "/admin/link_lastfm_do" }}
{{ $cbURL := printf "%s%s" .RequestRoot $cbPath }}
<div>{{ component "link" (props . "To" (printf "https://www.last.fm/api/auth/?api_key=%s&cb=%s" .CurrentLastFMAPIKey $cbURL)) }}link{{ end }}</div>
{{ end }}
{{ else }}
<p class="font-bold">api key not set</p>
{{ if not .User.IsAdmin }}
<p class="text-gray-500">please ask your admin to set it</p>
{{ end }}
{{ end }}
{{ if .User.IsAdmin }}
<p>{{ component "link" (props . "To" (path "/admin/update_lastfm_api_key" )) }}update api key{{ end }}</p>
{{ end }}
</div>
{{ end }}
{{ component "block" (props .
"Icon" "brain"
"Name" "listenbrainz"
"Desc" "scrobble to listenbrainz and compatible sites on a per user basis"
) }}
<div class="grid grid-cols-[1fr_1fr_auto] gap-2 items-center justify-items-end">
{{ if .User.ListenBrainzToken }}
<p class="text-gray-500 col-span-full">current status <span class="font-bold text-green-500">linked</span></p>
<form class="contents" action="{{ path "/admin/unlink_listenbrainz_do" }}" method="post">
<input class="col-span-full" type="submit" value="unlink">
</form>
{{ else }}
<p class="text-gray-500 col-span-full">current status <span class="font-bold text-red-400">unlinked</span></p>
<form class="contents" action="{{ path "/admin/link_listenbrainz_do" }}" method="post">
<input type="text" name="url" placeholder="server addr" value="{{ default .DefaultListenBrainzURL .User.ListenBrainzURL }}">
<input type="text" name="token" placeholder="api key" value="{{ .User.ListenBrainzToken }}">
<input type="submit" value="update">
</form>
{{ end }}
</div>
{{ end }}
{{ if .User.IsAdmin }}
{{ component "block" (props .
"Icon" "rss"
"Name" "podcasts"
"Desc" "you can add podcasts rss feeds here"
) }}
<div class="grid grid-cols-[auto_auto_min-content] md:grid-cols-[5fr_3fr_auto_auto] gap-2 items-center justify-items-end">
{{ range $pref := .Podcasts }}
<div class="ellipsis">{{ $pref.Title }}</div>
<form class="contents" action="{{ printf "/admin/update_podcast_do?id=%d" $pref.ID | path }}" method="post">
<select class="auto-submit" name="setting">
<option value="latest" {{ if eq $pref.AutoDownload "latest" }}selected="selected"{{ end }}>download latest</option>
<option value="none" {{ if eq (default "none" $pref.AutoDownload) "none" }}selected="selected"{{ end }}>no auto download</option>
</select>
</form>
<form class="hidden md:contents" action="{{ printf "/admin/download_podcast_do?id=%d" $pref.ID | path }}" method="post">
<input type="submit" value="download all">
</form>
<form class="contents" action="{{ printf "/admin/delete_podcast_do?id=%d" $pref.ID | path }}" method="post">
<input type="submit" value="delete">
</form>
{{ end }}
<form class="contents" action="{{ path "/admin/add_podcast_do" }}" method="post">
<input class="md:col-start-2 col-span-2" type="text" name="feed" placeholder="rss feed url">
<input type="submit" value="add new">
</form>
</div>
{{ end }}
{{ end }}
{{ if .User.IsAdmin }}
{{ component "block" (props .
"Icon" "rss"
"Name" "internet radio stations"
"Desc" "you can add and update internet radio stations here"
) }}
<div class="grid grid-cols-[1fr_1fr_min-content_min-content] md:grid-cols-[1fr_1fr_1fr_auto_auto] gap-2 items-center justify-items-end">
{{ range $pref := .InternetRadioStations }}
<form class="contents" action="{{ printf "/admin/update_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post">
<input class="col-span-full md:col-auto" type="text" name="name" value={{ $pref.Name }}>
<input type="text" name="streamURL" placeholder="stream url" value={{ $pref.StreamURL }}>
<input type="text" name="homepageURL" placeholder="homepage url" value={{ $pref.HomepageURL }}>
<input type="submit" value="update">
</form>
<form class="contents" action="{{ printf "/admin/delete_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post">
<input type="submit" value="delete">
</form>
{{ end }}
<form class="contents" action="{{ path "/admin/add_internet_radio_station_do" }}" method="post">
<input type="text" name="name" placeholder="name">
<input type="text" name="streamURL" placeholder="stream url">
<input type="text" name="homepageURL" placeholder="homepage url">
<input class="col-auto md:col-span-2" type="submit" value="add">
</form>
</div>
{{ end }}
{{ end }}
{{ component "block" (props .
"Icon" "list"
"Name" "playlists"
"Desc" "choose a local <span class='italic text-gray-800'>.m3u8</span> file containing paths to music files that gonic has scanned. paths should be absolute, prefixed by the <span class='italic text-gray-800'>music-path</span> option that you started gonic with. a playlist will be created from the file and available to subsonic clients"
) }}
{{ if eq (len .Playlists) 0 }}
<span class="text-gray-500">no playlists yet</span>
{{ end }}
<div class="grid grid-cols-[auto_1fr_auto] md:grid-cols-[auto_repeat(3,min-content)] gap-x-3 gap-y-2 items-center justify-items-end">
{{ range $i, $playlist := .Playlists }}
<div class="text-right ellipsis">{{ $playlist.Name }}</div>
<div class="text-gray-500 whitespace-nowrap">({{ $playlist.TrackCount }} tracks)</div>
<div class="text-right text-gray-500 whitespace-nowrap hidden md:block" title="{{ $playlist.CreatedAt }}">{{ $playlist.CreatedAt | dateHuman }}</div>
<form class="contents" action="{{ printf "/admin/delete_playlist_do?id=%d" $playlist.ID | path }}" method="post">
<input type="submit" value="delete">
</form>
{{ end }}
<form class="col-span-full relative pointer-events-auto" enctype="multipart/form-data" action="{{ path "/admin/upload_playlist_do" }}" method="post">
<input class="auto-submit absolute opacity-0" name="playlist-files" type="file" multiple />
<input type="button" value="choose m3u8">
</form>
</div>
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,13 @@
{{ component "layout" . }}
{{ component "block" (props .
"Icon" "user"
"Name" "login"
"Desc" "if you are logging in as an admin, the default credentials can be found in the readme"
) }}
<form class="flex flex-col md:flex-row gap-2 items-end" action="{{ path "/admin/login_do" }}" method="post">
<input class="text-center" type="text" id="username" name="username" placeholder="username">
<input class="text-center" type="password" id="password" name="password" placeholder="password">
<input type="submit" value="login">
</form>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,3 @@
{{ component "layout" . }}
<p class="bg-red-100 p-4 text-center">page not found</p>
{{ end }}

View File

@@ -0,0 +1,21 @@
{{ component "layout" . }}
{{ component "layout_user" . }}
{{ component "block" (props .
"Icon" "user"
"Name" "update last.fm api keys"
"Desc" "you can get an api key from last.fm here <a class='text-blue-500' href='https://www.last.fm/api/account/create' target='_blank' rel='noopener noreferrer'>here</a>. note, only the <span class='italic text-gray-800'>application name</span> field is required"
) }}
<div class="flex flex-col gap-2 items-end">
<p class="text-gray-500">current key <span class="font-bold text-gray-800 italic">{{ default "not set" .CurrentLastFMAPIKey }}</span></p>
<p class="text-gray-500">current secret <span class="font-bold text-gray-800 italic">{{ default "not set" .CurrentLastFMAPISecret }}</span></p>
<form class="contents" action="{{ path "/admin/update_lastfm_api_key_do" }}" method="post">
<input type="text" id="api_key" name="api_key" placeholder="new key">
<input type="text" id="secret" name="secret" placeholder="new secret">
<input type="submit" value="update">
</form>
</div>
{{ end }}
{{ end }}
{{ end }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -0,0 +1,3 @@
for (const input of document.querySelectorAll("input.auto-submit, select.auto-submit") || []) {
input.onchange = (e) => e.target.form.submit();
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
@tailwind base;
form,
input,
select {
all: unset;
appearance: none;
display: block;
}
a {
text-decoration: none;
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 500;
src: local(""),
url("/admin/static/inconsolata-v31-latin-500.woff2") format("woff2"),
url("/admin/static/inconsolata-v31-latin-500.woff") format("woff");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 600;
src: local(""),
url("/admin/static/inconsolata-v31-latin-600.woff2") format("woff2"),
url("/admin/static/inconsolata-v31-latin-600.woff") format("woff");
}
@tailwind components;
@tailwind utilities;
a {
@apply text-blue-500;
}
input[type],
select {
@apply h-6 px-2 leading-[1.5] w-full min-w-[3rem] md:min-w-[8rem] box-border bg-white text-gray-600 shadow-none border-0 outline outline-1 outline-gray-400/50 cursor-pointer overflow-hidden whitespace-nowrap text-ellipsis;
}
input[type="button"],
input[type="submit"] {
@apply text-center w-[6rem] md:w-[8rem] font-bold;
}
.ellipsis {
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap;
}

View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["*.tmpl", "**/*.tmpl"],
theme: {
screens: {
sm: "100%",
md: `870px`,
},
fontFamily: {
mono: ["Inconsolata", "monospace"],
},
},
};