From 8d32f78aae1dd4c1a398ec4bb45b9743f871eee7 Mon Sep 17 00:00:00 2001 From: beerpiss Date: Wed, 26 Jul 2023 15:15:29 +0700 Subject: [PATCH] Add a landing page for source websites --- .goreleaser.yaml | 2 +- cmd/aidoku/cmd/build.go | 12 +- cmd/aidoku/cmd/serve.go | 2 +- internal/build/build.go | 71 +++++- internal/build/web/index.html.tmpl | 89 ++++++++ internal/build/web/scripts/elements.js | 4 + internal/build/web/scripts/index.js | 109 +++++++++ internal/build/web/styles/index.css | 296 +++++++++++++++++++++++++ 8 files changed, 579 insertions(+), 6 deletions(-) create mode 100644 internal/build/web/index.html.tmpl create mode 100644 internal/build/web/scripts/elements.js create mode 100644 internal/build/web/scripts/index.js create mode 100644 internal/build/web/styles/index.css diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9335110..18cacbf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -25,7 +25,7 @@ builds: - -trimpath hooks: post: - - rice append -i ./cmd/aidoku/cmd -i ./internal/templates --exec "{{ .Path }}" + - rice append -i ./cmd/aidoku/cmd -i ./internal/templates -i ./internal/build --exec "{{ .Path }}" ldflags: | -s -w -X github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd.version={{.Version}} diff --git a/cmd/aidoku/cmd/build.go b/cmd/aidoku/cmd/build.go index 75aebba..a1cb055 100644 --- a/cmd/aidoku/cmd/build.go +++ b/cmd/aidoku/cmd/build.go @@ -17,8 +17,13 @@ var buildCmd = &cobra.Command{ if ForceColor { color.NoColor = false } - output, _ := cmd.Flags().GetString("output") - return build.BuildWrapper(args, output) + flags := cmd.Flags() + + output, _ := flags.GetString("output") + web, _ := flags.GetBool("web") + webTitle, _ := flags.GetString("web-title") + + return build.BuildWrapper(args, output, web, webTitle) }, } @@ -26,6 +31,9 @@ func init() { rootCmd.AddCommand(buildCmd) buildCmd.Flags().StringP("output", "o", "public", "Output folder") + buildCmd.Flags().BoolP("web", "w", false, "Bundle a landing page for the source list") + buildCmd.Flags().String("web-title", "An Aidoku source list", "Title of the landing page") + buildCmd.MarkZshCompPositionalArgumentFile(1, "*.aix") buildCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"aix"}, cobra.ShellCompDirectiveFilterFileExt diff --git a/cmd/aidoku/cmd/serve.go b/cmd/aidoku/cmd/serve.go index 5af589a..d2e2289 100644 --- a/cmd/aidoku/cmd/serve.go +++ b/cmd/aidoku/cmd/serve.go @@ -43,7 +43,7 @@ var serveCmd = &cobra.Command{ output, _ := cmd.Flags().GetString("output") port, _ := cmd.Flags().GetString("port") - build.BuildWrapper(args, output) + build.BuildWrapper(args, output, false, "") fmt.Println("Listening on these addresses:") if address == "0.0.0.0" { diff --git a/internal/build/build.go b/internal/build/build.go index 12d518a..1eab9ca 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "html/template" "io" "os" "sort" @@ -12,6 +13,7 @@ import ( "sync" "github.com/Aidoku/aidoku-cli/internal/common" + rice "github.com/GeertJohan/go.rice" "github.com/fatih/color" "github.com/segmentio/fasthash/fnv1a" "github.com/valyala/fastjson" @@ -29,7 +31,11 @@ type source struct { MaxVersion string `json:"maxAppVersion,omitempty"` } -func BuildWrapper(zipPatterns []string, output string) error { +type WebTemplateArguments struct { + Title string +} + +func BuildWrapper(zipPatterns []string, output string, web bool, webTitle string) error { os.RemoveAll(output) fileList := common.ProcessGlobs(zipPatterns) if len(fileList) == 0 { @@ -42,7 +48,68 @@ func BuildWrapper(zipPatterns []string, output string) error { } os.MkdirAll(output+"/icons", os.FileMode(0777)) os.MkdirAll(output+"/sources", os.FileMode(0777)) - return BuildSource(fileList, output) + + err = BuildSource(fileList, output) + if err != nil { + return err + } + + if web { + err = BuildWeb(webTitle, output) + if err != nil { + return err + } + } + + return nil +} + +func BuildWeb(webTitle string, output string) error { + box := rice.MustFindBox("web") + + bytes := box.MustBytes("index.html.tmpl") + + tmpl, err := template.New("index").Parse(string(bytes)) + if err != nil { + return err + } + + args := WebTemplateArguments{ + Title: webTitle, + } + + file, err := os.Create(output + "/index.html") + if err != nil { + return err + } + + err = tmpl.Execute(file, args) + if err != nil { + return err + } + + err = os.MkdirAll(output+"/scripts", os.FileMode(0777)) + if err != nil { + return err + } + err = os.MkdirAll(output+"/styles", os.FileMode(0777)) + if err != nil { + return err + } + + files := []string{"scripts/elements.js", "scripts/index.js", "styles/index.css"} + for _, file := range files { + bytes := box.MustBytes(file) + file, err := os.Create(output + "/" + file) + if err != nil { + return err + } + file.Write(bytes) + file.Sync() + file.Close() + } + + return nil } func BuildSource(zipFiles []string, output string) error { diff --git a/internal/build/web/index.html.tmpl b/internal/build/web/index.html.tmpl new file mode 100644 index 0000000..fec697e --- /dev/null +++ b/internal/build/web/index.html.tmpl @@ -0,0 +1,89 @@ + + + + + + {{ .Title }} + + + + + + + + + + +
+

{{ .Title }}

+ Add to Aidoku +
+
+ On a device with Aidoku installed, tap Add to Aidoku + or use the base URL to add this source list. +
+
+

Base URL:

+

+
+
+

Sources

+ + + +
+ + diff --git a/internal/build/web/scripts/elements.js b/internal/build/web/scripts/elements.js new file mode 100644 index 0000000..dd3f7f8 --- /dev/null +++ b/internal/build/web/scripts/elements.js @@ -0,0 +1,4 @@ +Promise.allSettled( + ["sl-button", "sl-input", "sl-select", "sl-option", "sl-checkbox", "sl-badge"] + .map(e => customElements.whenDefined(e)) +).then(() => document.body.classList.add("ready")); diff --git a/internal/build/web/scripts/index.js b/internal/build/web/scripts/index.js new file mode 100644 index 0000000..61a0371 --- /dev/null +++ b/internal/build/web/scripts/index.js @@ -0,0 +1,109 @@ +(function (scope) { + const namesInEnglish = new Intl.DisplayNames(["en"], { type: "language" }); + + /** + * + * @param {string} code + * @returns {[string, string]} + */ + function languageName(code) { + if (code == "multi") { + return ["Multi-Language", ""]; + } else { + const namesInNative = new Intl.DisplayNames([code], { type: "language" }); + + // All the language codes are probably valid if they got merged. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return [namesInEnglish.of(code), namesInNative.of(code)]; + } + } + + /** + * + * @param {string} code + * @returns {string} + */ + function fullLanguageName(code) { + const names = languageName(code); + return names[1] + ? code !== "en" + ? `${names[0]} - ${names[1]}` + : names[0] + : names[0]; + } + + const LoadingStatus = { + Loading: "loading", + Loaded: "loaded", + Error: "error", + }; + + document.addEventListener("alpine:init", () => { + Alpine.store("sourceUrl", window.location.href.replace(window.location.hash, "")) + Alpine.store("addUrl", `aidoku://addSourceList?url=${window.location.href.replace(window.location.hash, "")}`) + + Alpine.data("sourceList", () => ({ + sources: [], + languages: [], + loading: LoadingStatus.Loading, + + LoadingStatus, + languageName, + fullLanguageName, + + // options + filtered: [], + query: "", + selectedLanguages: [], + nsfw: true, + + async init() { + try { + const res = await fetch(`./index.min.json`); + this.sources = (await res.json()).sort((lhs, rhs) => { + if (lhs.lang === "multi" && rhs.lang !== "multi") { + return -1; + } + if (lhs.lang !== "multi" && rhs.lang === "multi") { + return 1; + } + if (lhs.lang === "en" && rhs.lang !== "en") { + return -1; + } + if (rhs.lang === "en" && lhs.lang !== "en") { + return 1; + } + + const [langLhs] = languageName(lhs.lang); + const [langRhs] = languageName(rhs.lang); + if (langLhs < langRhs) { + return -1; + } + if (langRhs > langLhs) { + return 1; + } + return lhs.name < rhs.name ? -1 : lhs.name === rhs.name ? 0 : 1; + }); + this.languages = [...new Set(this.sources.map((source) => source.lang))]; + this.loading = LoadingStatus.Loaded; + } catch { + this.loading = LoadingStatus.Error; + } + }, + + updateFilteredList() { + this.filtered = this.sources + .filter((item) => + this.query + ? item.name.toLowerCase().includes(this.query.toLowerCase()) || + item.id.toLowerCase().includes(this.query.toLowerCase()) + : true + ) + .filter((item) => (this.nsfw ? true : (item.nsfw ?? 0) <= 1)) + .filter((item) => + this.selectedLanguages.length ? this.selectedLanguages.includes(item.lang) : true + ); + } + })) + }) +})(window); diff --git a/internal/build/web/styles/index.css b/internal/build/web/styles/index.css new file mode 100644 index 0000000..887ed27 --- /dev/null +++ b/internal/build/web/styles/index.css @@ -0,0 +1,296 @@ +* { + --aidoku-color: #ff375f; + font-family: -apple-system, BlinkMacSystemFont, Inter, system-ui, sans-serif; +} + +/* #region Add to Aidoku button */ +.add-to-aidoku::part(base) { + background: #ff375f0f; + border: none; +} + +.add-to-aidoku::part(base):hover { + background: rgba(255, 55, 95, 0.15); +} + +.add-to-aidoku::part(base):active { + background: rgba(255, 55, 95, 0.25); +} + +.add-to-aidoku::part(base):focus-visible { + box-shadow: 0 0 0 3px solid rgba(255, 55, 95, 0.33); +} + +.add-to-aidoku::part(label) { + color: var(--aidoku-color); +} +/* #endregion */ + +body { + margin: 0.75rem auto; + max-width: 42rem; + opacity: 0; +} + +body.ready { + opacity: 1; +} + +header { + margin: 1rem auto; + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + text-align: center; + + font-size: 1.25rem; + line-height: 1.75rem; +} + +header p { + color: var(--aidoku-color); + font-weight: 600; + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; + margin-top: 0.5rem; +} + +.description { + width: 91.6667%; + margin: auto 2rem; +} + +.description a { + color: var(--aidoku-color); + text-decoration: none; +} + +.base-url { + width: 91.6667%; + margin: 0.75rem auto; + padding: 0.75rem 1rem; + + border-style: solid; + border-radius: 0.125rem; + border-width: 1px; + border-color: rgb(96 165 250); + + background-color: rgb(239 246 255); +} + +.base-url__description { + font-weight: 600; + margin: 0; + margin-bottom: 0.5rem; +} + +.base-url__url { + margin: 0; +} + +.sources { + margin: 0.75rem auto; +} + +.sources h1 { + font-weight: 600; + font-size: 1.875rem; + line-height: 2.25rem; + padding-bottom: 1rem; + margin: 0; +} + +.sources__search { + padding-bottom: 0.25rem; +} + +.sources__search * { + margin-bottom: 0.25rem; +} + +.sources__status { + display: flex; + align-items: center; + justify-content: center; +} + +.sources__item { + display: flex; + align-items: center; + + padding: 0.5rem 0.5rem; +} + +@media (min-width: 1024px) { + .sources__item { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +.sources__item:hover .sources__anchor { + opacity: 1; +} + +.sources__anchor { + opacity: 0; + float: left; + + margin-top: auto; + margin-bottom: auto; + margin-left: -1.5rem; + + padding-left: 0.5rem; + padding-right: 0.5rem; + + font-size: 1.25rem; + font-weight: 400; + line-height: 1.75rem; + + color: var(--aidoku-color); +} + +.sources__anchor:hover { + text-decoration-line: underline; +} + +.sources__icon-wrapper { + margin-top: auto; + margin-bottom: auto; +} + +.sources__icon { + display: block; + max-width: 100%; + margin-right: 0.5rem; + border-radius: 0.5rem; +} + +.sources__info { + display: flex; + flex: 1 1 0%; + flex-direction: column; +} + +.sources__name { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; + font-weight: 600; +} + +.sources__name sl-badge { + font-size: 0.75rem; + line-height: 0.875rem; +} + +.sources__version { + font-size: 0.875rem; + line-height: 1.25rem; + color: rgb(156 163 175); +} + +.sources__download { + margin: auto auto; +} + +.sources__download sl-button::part(base) { + background: var(--sl-color-gray-100); + border: none; +} + +.sources__download sl-button::part(base):hover { + background: var(--sl-color-gray-200); +} + +.sources__download sl-button::part(base):active { + background: var(--sl-color-gray-300); +} + +.sources__download sl-button::part(base):focus-visible { + box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.33); +} + +.sources__download sl-button::part(label) { + color: var(--aidoku-color); +} + + +/* #region Spinner */ +@keyframes fade { + from { + opacity: 1; + } + to { + opacity: 0.25; + } +} + +.spinner { + position: relative; + display: inline-block; + height: 3.5rem; + width: 3.5rem; +} + +.spinner>div { + position: absolute; + left: 49%; + top: 43%; + height: 16%; + width: 6%; + + border-radius: 50px; + + background-color: #78787a; + opacity: 0; + box-shadow: 0 0 3px rgba(0,0,0,0.2); + + animation: fade 1s linear infinite; +} + +.spinner>div.bar0 { + transform: rotate(0deg) translate(0, -130%); + animation-delay: 0s; +} + +.spinner>div.bar1 { + transform: rotate(45deg) translate(0, -130%); + animation-delay: 0.125s; +} + +.spinner>div.bar2 { + transform: rotate(90deg) translate(0, -130%); + animation-delay: 0.25s; +} + +.spinner>div.bar3 { + transform: rotate(135deg) translate(0, -130%); + animation-delay: 0.375s; +} + +.spinner>div.bar4 { + transform: rotate(180deg) translate(0, -130%); + animation-delay: 0.5s; +} + +.spinner>div.bar5 { + transform: rotate(225deg) translate(0, -130%); + animation-delay: 0.625s; +} + +.spinner>div.bar6 { + transform: rotate(270deg) translate(0, -130%); + animation-delay: 0.75s; +} + +.spinner>div.bar7 { + transform: rotate(315deg) translate(0, -130%); + animation-delay: 0.875s; +} +/* #endregion */