Skip to content

Commit

Permalink
Add a landing page for source websites
Browse files Browse the repository at this point in the history
  • Loading branch information
beer-psi committed Jul 26, 2023
1 parent eb715cd commit 8d32f78
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
12 changes: 10 additions & 2 deletions cmd/aidoku/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@ 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)
},
}

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
Expand Down
2 changes: 1 addition & 1 deletion cmd/aidoku/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
71 changes: 69 additions & 2 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"os"
"sort"
"strings"
"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"
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
89 changes: 89 additions & 0 deletions internal/build/web/index.html.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.5.2/cdn/themes/light.css" />
<link rel="stylesheet" href="styles/index.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.5.2/cdn/shoelace-autoloader.js"></script>

<script defer src="scripts/index.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
<script type="module" src="scripts/elements.js"></script>
</head>
<body>
<header>
<p>{{ .Title }}</p>
<sl-button class="add-to-aidoku" x-data :href="$store.addUrl">Add to Aidoku</sl-button>
</header>
<div class="description">
On a device with Aidoku installed, tap <a x-data :href="$store.addUrl">Add to Aidoku</a>
or use the base URL to add this source list.
</div>
<div class="base-url" variant="primary">
<p class="base-url__description">Base URL:</p>
<p class="base-url__url" x-data x-text="$store.sourceUrl"></p>
</div>
<div x-data="sourceList" class="sources">
<h1>Sources</h1>
<template x-if="loading === LoadingStatus.Loading">
<div class="sources__status">
<div aria-label="Loading" class="spinner">
<div class="bar0"></div>
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
<div class="bar4"></div>
<div class="bar5"></div>
<div class="bar6"></div>
<div class="bar7"></div>
</div>
</div>
</template>
<template x-if="loading === LoadingStatus.Error">
<div class="sources__status">
<div class="text-red-500">
<p>Could not load sources.</p>
<p>This source list might not work in the app.</p>
</div>
</div>
</template>
<template x-if="loading === LoadingStatus.Loaded">
<div>
<div class="sources__search" x-effect="updateFilteredList">
<sl-input placeholder="Search by name or ID..." x-model="query"></sl-input>
<sl-select placeholder="Filter by languages" multiple clearable @sl-change="selectedLanguages = event.target.value">
<template x-for="(language, index) in languages" :key="language">
<sl-option :value="language" x-text="fullLanguageName(language)"></sl-option>
</template>
</sl-select>
<sl-checkbox @sl-change="nsfw = event.target.checked" :checked="nsfw">Show NSFW sources?</sl-checkbox>
</div>
<div class="sources__list">
<template x-for="source in filtered" :key="source.id">
<div class="sources__item">
<a :href="`#${source.id}`" class="sources__anchor">#</a>
<div class="sources__icon-wrapper">
<img class="sources__icon" :alt="`Icon for ${source.name}`" :src="`./icons/${source.icon}`" loading="lazy" width="42" height="42">
</div>
<div class="sources__info">
<div class="sources__name">
<span x-text="source.name"></span>
<span class="sources__version" x-text="`v${source.version}`"></span>
<sl-badge variant="danger" pill x-cloak x-show="source.nsfw === 2">18+</sl-badge>
</div>
<div class="sources__version" x-text="languageName(source.lang)[0]"></div>
</div>
<div class="sources__download">
<sl-button size="small" pill download :href="`./sources/${source.file}`">Download</sl-button>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</body>
</html>
4 changes: 4 additions & 0 deletions internal/build/web/scripts/elements.js
Original file line number Diff line number Diff line change
@@ -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"));
109 changes: 109 additions & 0 deletions internal/build/web/scripts/index.js
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit 8d32f78

Please sign in to comment.