necessário

This commit is contained in:
silva guimaraes 2025-07-24 19:31:03 -03:00
parent 92b0a902ca
commit 7edb0a17ed
5 changed files with 199 additions and 162 deletions

64
main.go
View file

@ -111,7 +111,7 @@ func main() {
Port: ":2323", Port: ":2323",
Root: "/home/xi/seed/gallery-dl/", Root: "/home/xi/seed/gallery-dl/",
Galleries: make([]gallery.Gallery, 0, 50), Galleries: make([]gallery.Gallery, 0, 50),
UniqueTags: make(map[gallery.Tag]int), UniqueTags: make(map[string]int),
UniqueArtists: make(map[string]int), UniqueArtists: make(map[string]int),
UniqueGroups: make(map[string]int), UniqueGroups: make(map[string]int),
UniqueParodies: make(map[string]int), UniqueParodies: make(map[string]int),
@ -276,35 +276,35 @@ func main() {
_ = png.Encode(io.MultiWriter(imageCache, w), destImage) _ = png.Encode(io.MultiWriter(imageCache, w), destImage)
}) })
// http.DefaultServeMux.HandleFunc("POST /filter/{tag}/{sex}", func(w http.ResponseWriter, r *http.Request) { // http.DefaultServeMux.HandleFunc("GET /filter/{tag}/{sex}", func(w http.ResponseWriter, r *http.Request) {
// tagLookup := r.PathValue("tag") // tagLookup := r.PathValue("tag")
// if tagLookup == "" { // if tagLookup == "" {
// panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path))
// } // }
// if tagLookup == "clear" { // if tagLookup == "clear" {
// state.Filtered = []gallery.Tag{} // state.Filtered = []gallery.Tag{}
// view.GalleriesListing(state.Galleries).Render(w) // view.SearchResults(state.Galleries).Render(w)
// return // return
// } // }
// tagSexLookup := r.PathValue("sex") // tagSexLookup := r.PathValue("sex")
// if tagSexLookup == "" { // if tagSexLookup == "" {
// panic(fmt.Errorf("tag sex could not be found: %s", r.URL.Path)) // panic(fmt.Errorf("tag sex could not be found: %s", r.URL.Path))
// } // }
// sex := gallery.ToGender(tagSexLookup) // sex := gallery.Gender(tagSexLookup)
// i := slices.IndexFunc(state.TagKeys, func(a gallery.Tag) bool { // i := slices.IndexFunc(state.TagKeys, func(a gallery.Tag) bool {
// return a.Name == tagLookup && a.Sex == sex // return a.Name == tagLookup && a.Sex == sex
// }) // })
// if i < 0 { // if i < 0 {
// panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path))
// } // }
// tag := state.TagKeys[i] // tag := state.TagKeys[i]
// i = slices.Index(state.Filtered, tag) // i = slices.Index(state.Filtered, tag)
// if i < 0 { // if i < 0 {
// state.Filtered = append(state.Filtered, tag) // state.Filtered = append(state.Filtered, tag)
// } else { // } else {
// state.Filtered = slices.Delete(state.Filtered, i, i+1) // state.Filtered = slices.Delete(state.Filtered, i, i+1)
// } // }
// view.GalleriesListing(state.FilterGalleries()).Render(w) // view.SearchResults(state.FilterGalleries()).Render(w)
// }) // })
http.DefaultServeMux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) { http.DefaultServeMux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) {
@ -328,7 +328,7 @@ func main() {
query := r.FormValue("search") query := r.FormValue("search")
log.Println("search form result:", query) log.Println("search form result:", query)
if query == "" { if query == "" {
view.SearchResults(state.Galleries, 0).Render(w) view.SearchResults(state.Galleries).Render(w)
return return
} }
// ranks := fuzzy.RankFindFold(search, state.GalleryNames) // ranks := fuzzy.RankFindFold(search, state.GalleryNames)
@ -362,7 +362,7 @@ func main() {
} }
galleries = append(galleries, state.Galleries[r.OriginalIndex]) galleries = append(galleries, state.Galleries[r.OriginalIndex])
} }
view.SearchResults(galleries, 0).Render(w) view.SearchResults(galleries).Render(w)
}) })
log.Fatal(server.Serve(ln)) log.Fatal(server.Serve(ln))

View file

@ -3,19 +3,19 @@ package state
import ( import (
"fmt" "fmt"
"goreader/gallery" "goreader/gallery"
"maps"
"slices" "slices"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
) )
type State struct { type State struct {
Port string Port string
Root string Root string
CacheDir string CacheDir string
Galleries []gallery.Gallery Galleries []gallery.Gallery
GalleryNames []string GalleryNames []string
UniqueTags map[gallery.Tag]int // UniqueTags map[gallery.Tag]int
UniqueTags map[string]int
UniqueArtists map[string]int UniqueArtists map[string]int
UniqueGroups map[string]int UniqueGroups map[string]int
UniqueParodies map[string]int UniqueParodies map[string]int
@ -28,11 +28,11 @@ func (s *State) AddGallery(g gallery.Gallery) error {
s.Galleries = append(s.Galleries, g) s.Galleries = append(s.Galleries, g)
s.GalleryNames = append(s.GalleryNames, g.Name()) s.GalleryNames = append(s.GalleryNames, g.Name())
slices.SortFunc(s.Galleries, func(a, b gallery.Gallery) int { slices.SortFunc(s.Galleries, func(a, b gallery.Gallery) int {
return a.CTime.Compare(b.CTime) return -a.CTime.Compare(b.CTime)
}) })
for _, tag := range g.Tags() { for _, tag := range g.Tags() {
s.UniqueTags[tag]++ s.UniqueTags[tag.Name]++
} }
for _, artist := range g.Artists() { for _, artist := range g.Artists() {
s.UniqueArtists[artist]++ s.UniqueArtists[artist]++
@ -43,7 +43,6 @@ func (s *State) AddGallery(g gallery.Gallery) error {
for _, parody := range g.Parodies() { for _, parody := range g.Parodies() {
s.UniqueParodies[parody]++ s.UniqueParodies[parody]++
} }
s.TagKeys = slices.Collect(maps.Keys(s.UniqueTags))
return nil return nil
} }

View file

@ -6,6 +6,7 @@ import (
"goreader/gallery" "goreader/gallery"
"goreader/state" "goreader/state"
"strconv" "strconv"
"strings"
"github.com/silva-guimaraes/gtag" "github.com/silva-guimaraes/gtag"
) )
@ -37,15 +38,22 @@ func tag(t gallery.Tag, state *state.State) *gtag.Tag {
backgroundClass = "female" backgroundClass = "female"
} }
d := gtag.New("a"). d := gtag.New("a").
Href("javascript:void(0)"). Href(fmt.Sprintf("javascript:filter_tags('%s')", strings.TrimSpace(t.Name) /* FIXME */)).
Class("tag"). Class("tag")
SetAttr("hx-post", fmt.Sprintf("/filter/%s/%s", t.Name, t.Sex)).
SetAttr("hx-target", "#listing").
SetAttr("hx-swap", "outerHTML")
{ {
d.Tag("span").Text(gender).Class("name", backgroundClass) d.Tag("span").Text(gender).Class("name", backgroundClass)
d.Tag("span").Text(t.Name).Class("name") d.Tag("span").Text(t.Name).Class("name")
d.Tag("span").Text(strconv.Itoa(state.UniqueTags[t])).Class("name", backgroundClass) d.Tag("span").Text(strconv.Itoa(state.UniqueTags[t.Name])).Class("name", backgroundClass)
}
return d
}
func artistTag(artistName string) *gtag.Tag {
d := gtag.New("a").
Href(fmt.Sprintf("javascript:filter_artist('%s')", artistName /* FIXME */)).
Class("tag")
{
d.Tag("span").Text(artistName).Class("name")
} }
return d return d
} }
@ -55,7 +63,8 @@ func thumbnail(src string) *gtag.Tag {
} }
func fullThumbnail(g gallery.Gallery) *gtag.Tag { func fullThumbnail(g gallery.Gallery) *gtag.Tag {
return thumbnail(fmt.Sprintf("/page/%s/1", g.Uuid())) thumb := thumbnail(fmt.Sprintf("/page/%s/1", g.Uuid()))
return gtag.Div().Id("image-wrapper").Append(thumb)
} }
func smallThumbnail(g gallery.Gallery) *gtag.Tag { func smallThumbnail(g gallery.Gallery) *gtag.Tag {
@ -71,9 +80,21 @@ func InspectorInfo(g gallery.Gallery, state *state.State) *gtag.Tag {
d.Tag("a"). d.Tag("a").
SetAttr("href", fmt.Sprintf("/read/%s", g.Uuid())). SetAttr("href", fmt.Sprintf("/read/%s", g.Uuid())).
SetAttr("hx-boost", "false"). SetAttr("hx-boost", "false").
SetAttr("style", "color: white"). Tag("h1").
Tag("h1").Text(g.Name()) SetAttr("style", "color: white; margin: 10px 0 10px 0;").
d.Tag("h2").Text(g.JpName()) Text(g.Name())
d.Tag("h2").Style("margin: 0").Text(g.JpName())
d.Tag("h3").
Text(fmt.Sprintf("Pages: %d", len(g.Images()))).
Style("font-size: 14px; ; margin: 0;")
d.Tag("h2").Text("Artists")
artists := d.Div()
{
for _, a := range g.Artists() {
artists.Append(artistTag(a))
}
}
d.Tag("h2").Text("Tags")
tags := d.Div() tags := d.Div()
{ {
for _, t := range g.Tags() { for _, t := range g.Tags() {
@ -85,10 +106,16 @@ func InspectorInfo(g gallery.Gallery, state *state.State) *gtag.Tag {
} }
func cover(g gallery.Gallery) *gtag.Tag { func cover(g gallery.Gallery) *gtag.Tag {
var s []string
for _, tag := range g.Tags() {
s = append(s, fmt.Sprintf("'%s'", strings.TrimSpace(tag.Name)) /* FIXME */)
}
dataset := fmt.Sprintf("[%s]", strings.Join(s, ", "))
a := gtag.New("a"). a := gtag.New("a").
Class("cover"). Class("cover").
Id(fmt.Sprintf("cover-%s", g.Uuid())). Id(fmt.Sprintf("cover-%s", g.Uuid())).
SetAttr("href", fmt.Sprintf("/read/%s", g.Uuid())). SetAttr("href", fmt.Sprintf("/read/%s", g.Uuid())).
SetAttr("data-tags", dataset).
SetAttr("hx-get", fmt.Sprintf("/details/%s", g.Uuid())). SetAttr("hx-get", fmt.Sprintf("/details/%s", g.Uuid())).
SetAttr("onmouseenter", "inspectSetTimeout(event)"). SetAttr("onmouseenter", "inspectSetTimeout(event)").
SetAttr("onmouseleave", "inspectClearTimeout(event)"). SetAttr("onmouseleave", "inspectClearTimeout(event)").
@ -101,22 +128,19 @@ func cover(g gallery.Gallery) *gtag.Tag {
return a return a
} }
const galleriesPerPage = 50 const galleriesPerPage = 70
func SearchResults(galleries []gallery.Gallery, page int) *gtag.Tag { func SearchResults(galleries []gallery.Gallery) *gtag.Tag {
glm := max(len(galleries)-1, 0) s := gtag.New("section")
recent := galleries[min(page*galleriesPerPage, glm):min((page+1)*galleriesPerPage, glm)] results := s.Tag("section").Id("results")
m := gtag.New("main")
// HTMX faz com que isso receba [InspectorInfo] quando usuário paira o
// mouse sobre algum cover
m.Tag("section").Id("inspector")
results := m.Tag("section").Id("results")
{ {
for _, g := range recent { results.Tag("header").Tag("h1").Text(fmt.Sprintf("Listing %d Galleries", len(galleries)))
results.Append(cover(g)) covers := results.Tag("main")
for _, g := range galleries {
covers.Append(cover(g))
} }
} }
return m return s
} }
func Stats(state *state.State) *gtag.Tag { func Stats(state *state.State) *gtag.Tag {
@ -153,80 +177,18 @@ func Index(state *state.State, page int) *gtag.Tag {
{ {
nav := body.Tag("nav").Class("container") nav := body.Tag("nav").Class("container")
{ {
nav.Tag("a").
Href("javascript:clear_filters()").
Text("clear filters")
nav.Div().Text("omakase v1") nav.Div().Text("omakase v1")
} }
center := body.Div().Id("center") center := body.Div().Id("center")
{ {
top := center.Tag("div").Class("top") d := center.Div()
{ // HTMX faz com que isso receba [InspectorInfo] quando usuário paira o
top.Div().Id("search-bar").VoidTag("input"). // mouse sobre algum cover
Class("ask-input"). d.Tag("section").Id("inspector")
SetAttr("type", "search"). d.Append(SearchResults(state.Galleries))
SetAttr("name", "search").
SetAttr("autocomplete", "false").
SetAttr("placeholder", "Slash to search...").
SetAttr("hx-post", "/search").
SetAttr("hx-trigger", "input changed delay:50ms, search").
SetAttr("hx-target", "#search-results")
header := top.Tag("header").Class("container")
{
header.Div().Id("omakase").
Tag("a").
SetAttr("href", "/random").
Text("おまかせ").
SetAttr("target", "_blank")
header.Tag("hr").Style("opacity: 0.2;")
header.Append(Stats(state))
}
}
ret := center.Tag("main").Id("search-results")
{
controls := ret.Tag("section").AddClass("container").Id("controls")
{
paging := controls.Div()
{
maxPages := len(state.Galleries) / galleriesPerPage
previousPage := gtag.New("a").
AddClass("page-control").
Text("<")
if page > 1 {
previousPage.SetAttr("href",
fmt.Sprintf("?page=%d", page-1),
)
}
paging.Append(previousPage)
p := page
for ; p < min(page+3, maxPages); p++ {
paging.Tag("a").
AddClass("page-control").
SetAttr("href",
fmt.Sprintf("?page=%d", p),
).Text(fmt.Sprint(p))
}
if len(state.Galleries) > 0 && p != maxPages {
paging.Tag("span").Text("...")
}
paging.Tag("a").
AddClass("page-control").
SetAttr("href",
fmt.Sprintf("?page=%d", maxPages),
).Text(fmt.Sprint(maxPages))
nextPage := gtag.New("a").
AddClass("page-control").
SetAttr("href", "?page=1").
Text(">")
if page < maxPages {
nextPage.SetAttr("href",
fmt.Sprintf("?page=%d", page+1),
)
} else if page == maxPages {
nextPage.SetAttr("disabled", "true")
}
paging.Append(nextPage)
}
}
ret.Append(SearchResults(state.Galleries, page))
}
} }
body.Tag("footer").P().Text(randomQuote()) body.Tag("footer").P().Text(randomQuote())
body.Tag("script").Asis(indexJavascript) body.Tag("script").Asis(indexJavascript)

View file

@ -10,13 +10,77 @@ hotkeys('/', {keyup: true}, function (event, _){
}); });
let filtered_tags = new Set();
/**
* @param {string} name
*/
function filter_tags(name) {
filtered_tags.add(name);
/**
* @type {NodeListOf<HTMLElement>}
*/
let covers = document.querySelectorAll('.cover');
for (let cover of covers) {
/**
* @type {Array<string>}
*/
let tags = eval(cover.dataset['tags']); // yeehaw!!! eu sou um cowboy yeehaw!!!
if (!(tags.includes(name))) {
cover.classList.add('hidden');
}
}
const url = new URL(window.location.href);
const new_params = new URLSearchParams([
['tags', Array.from(filtered_tags).join('&')]
// ...Array.from(url.searchParams.entries()),
]).toString();
window.history.replaceState(null, "", `${url.pathname}?${new_params}`);
}
/**
* @param {string} name
*/
function filter_artists(name) {
filtered_tags.add(name);
/**
* @type {NodeListOf<HTMLElement>}
*/
let covers = document.querySelectorAll('.cover');
for (let cover of covers) {
/**
* @type {Array<string>}
*/
let tags = eval(cover.dataset['tags']); // yeehaw!!! eu sou um cowboy yeehaw!!!
if (!(tags.includes(name))) {
cover.classList.add('hidden');
}
}
const url = new URL(window.location.href);
const new_params = new URLSearchParams([
['tags', Array.from(filtered_tags).join('&')]
// ...Array.from(url.searchParams.entries()),
]).toString();
window.history.replaceState(null, "", `${url.pathname}?${new_params}`);
}
// filtra tags na url da página
const url = new URL(window.location.href);
filtered_tags = new Set(url.searchParams.get('tags')?.split('&'));
filtered_tags?.forEach(filter_tags);
function clear_filters() {
window.history.replaceState(null, "", url.pathname);
document.querySelectorAll('.cover').forEach(x => x.classList.remove('hidden'));
filtered_tags = new Set();
}
/** /**
* @param {Event & {target: HTMLElement}} event * @param {Event & {target: HTMLElement}} event
*/ */
function inspectSetTimeout(event) { function inspectSetTimeout(event) {
let target = event.target; let target = event.target;
let id = setTimeout( let id = setTimeout(
() => { () => {
let targetId = '#' + target.id; let targetId = '#' + target.id;
@ -33,7 +97,7 @@ function inspectSetTimeout(event) {
500 500
); );
target.dataset['timeout'] = id target.dataset['timeout'] = id;
} }
/** /**
@ -41,6 +105,6 @@ function inspectSetTimeout(event) {
*/ */
function inspectClearTimeout(event) { function inspectClearTimeout(event) {
let target = event.target; let target = event.target;
clearTimeout(target.dataset['timeout']) clearTimeout(target.dataset['timeout']);
} }

View file

@ -12,6 +12,12 @@
--main-margin: 62px; --main-margin: 62px;
} }
* {
margin: 0;
font-size-adjust: ex-height 0.53;
box-sizing: border-box;
}
.page { .page {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
@ -20,7 +26,7 @@
} }
.hidden { .hidden {
display: none; display: none !important;
} }
#pages-container { #pages-container {
@ -94,12 +100,9 @@ body {
#index nav { #index nav {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
/* margin-bottom: 11px; */
/* background-color: var(--haughty-gray); */
border-bottom: 1px solid var(--silver-lining-white); border-bottom: 1px solid var(--silver-lining-white);
padding: 5px; padding: 5px;
/* box-shadow: black 1px 1px 1px 1px; */
} }
#index #content { #index #content {
@ -116,14 +119,7 @@ body {
padding: 20px; padding: 20px;
} }
#index #center {
margin: var(--main-margin) var(--main-margin) auto var(--main-margin);
}
.top { .top {
/* width: 100%; */
/* display: flex; */
/* flex-direction: row; */
display: flex; display: flex;
margin-bottom: 50px; margin-bottom: 50px;
} }
@ -172,7 +168,7 @@ div#search-bar {
} }
} }
#index header { #index > header {
width: min-content; width: min-content;
padding: 20px; padding: 20px;
font-family: sans-serif; font-family: sans-serif;
@ -233,10 +229,12 @@ footer {
.tag, .tag,
.caption { .caption {
font-family: Noto sans, sans-serif; font-family: sans-serif;
font-weight: 600; font-weight: 900;
color: white; color: white;
text-decoration-line: none; text-decoration-line: none;
margin: 2px;
border-radius: 3px 3px;
} }
.tag { .tag {
@ -244,6 +242,7 @@ footer {
color: var(--almost-white); color: var(--almost-white);
text-decoration-line: none; text-decoration-line: none;
padding: 0.13em; padding: 0.13em;
background-color: var(--main-shadow);
&>span:first-child { &>span:first-child {
font-family: monospace; font-family: monospace;
@ -251,9 +250,6 @@ footer {
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
&>span:nth-child(2) {
background-color: var(--haughty-gray);
}
&>span:last-child { &>span:last-child {
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
@ -283,6 +279,7 @@ footer {
.cover { .cover {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 11%;
} }
.caption { .caption {
@ -292,17 +289,21 @@ footer {
} }
img { img {
filter: blur(15px); filter: blur(30px);
}
#index img {
width: 100%;
} }
.thumbnail, .thumbnail,
.cover { .cover {
width: 148px; /* width: 148px; */
height: auto; height: auto;
opacity: 0.93; opacity: 0.93;
} }
#search-results { #index #center {
& #controls { & #controls {
width: calc(min-content / 2); width: calc(min-content / 2);
height: min-content; height: min-content;
@ -325,9 +326,9 @@ img {
} }
& #results { & #results {
display: grid; /* display: grid; */
width: 65%;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
width: 65%;
@media (min-width: 1200px) { @media (min-width: 1200px) {
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, 1fr);
@ -349,9 +350,20 @@ img {
Arial, Arial,
sans-serif; sans-serif;
& img {
& #image-wrapper {
position: relative;
width: 100%;
padding-top: 95vh;
overflow: hidden;
}
& #image-wrapper img {
position: absolute;
top: 0;
left: 0;
max-height: 95vh; max-height: 95vh;
max-width: 100%; max-width: 100%;
object-fit: cover;
/* width: auto; */ /* width: auto; */
/* height: auto; */ /* height: auto; */
} }
@ -374,7 +386,7 @@ img {
font-size: large; font-size: large;
} }
& div { & div:last-child {
margin-bottom: 100%; margin-bottom: 100%;
} }
} }