diff --git a/main.go b/main.go index f0d14b6..adcfaaa 100644 --- a/main.go +++ b/main.go @@ -111,7 +111,7 @@ func main() { Port: ":2323", Root: "/home/xi/seed/gallery-dl/", Galleries: make([]gallery.Gallery, 0, 50), - UniqueTags: make(map[gallery.Tag]int), + UniqueTags: make(map[string]int), UniqueArtists: make(map[string]int), UniqueGroups: make(map[string]int), UniqueParodies: make(map[string]int), @@ -276,35 +276,35 @@ func main() { _ = png.Encode(io.MultiWriter(imageCache, w), destImage) }) - // http.DefaultServeMux.HandleFunc("POST /filter/{tag}/{sex}", func(w http.ResponseWriter, r *http.Request) { - // tagLookup := r.PathValue("tag") - // if tagLookup == "" { - // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) - // } - // if tagLookup == "clear" { - // state.Filtered = []gallery.Tag{} - // view.GalleriesListing(state.Galleries).Render(w) - // return - // } - // tagSexLookup := r.PathValue("sex") - // if tagSexLookup == "" { - // panic(fmt.Errorf("tag sex could not be found: %s", r.URL.Path)) - // } - // sex := gallery.ToGender(tagSexLookup) - // i := slices.IndexFunc(state.TagKeys, func(a gallery.Tag) bool { - // return a.Name == tagLookup && a.Sex == sex - // }) - // if i < 0 { - // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) - // } - // tag := state.TagKeys[i] - // i = slices.Index(state.Filtered, tag) - // if i < 0 { - // state.Filtered = append(state.Filtered, tag) - // } else { - // state.Filtered = slices.Delete(state.Filtered, i, i+1) - // } - // view.GalleriesListing(state.FilterGalleries()).Render(w) + // http.DefaultServeMux.HandleFunc("GET /filter/{tag}/{sex}", func(w http.ResponseWriter, r *http.Request) { + // tagLookup := r.PathValue("tag") + // if tagLookup == "" { + // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) + // } + // if tagLookup == "clear" { + // state.Filtered = []gallery.Tag{} + // view.SearchResults(state.Galleries).Render(w) + // return + // } + // tagSexLookup := r.PathValue("sex") + // if tagSexLookup == "" { + // panic(fmt.Errorf("tag sex could not be found: %s", r.URL.Path)) + // } + // sex := gallery.Gender(tagSexLookup) + // i := slices.IndexFunc(state.TagKeys, func(a gallery.Tag) bool { + // return a.Name == tagLookup && a.Sex == sex + // }) + // if i < 0 { + // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) + // } + // tag := state.TagKeys[i] + // i = slices.Index(state.Filtered, tag) + // if i < 0 { + // state.Filtered = append(state.Filtered, tag) + // } else { + // state.Filtered = slices.Delete(state.Filtered, i, i+1) + // } + // view.SearchResults(state.FilterGalleries()).Render(w) // }) http.DefaultServeMux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) { @@ -328,7 +328,7 @@ func main() { query := r.FormValue("search") log.Println("search form result:", query) if query == "" { - view.SearchResults(state.Galleries, 0).Render(w) + view.SearchResults(state.Galleries).Render(w) return } // ranks := fuzzy.RankFindFold(search, state.GalleryNames) @@ -362,7 +362,7 @@ func main() { } galleries = append(galleries, state.Galleries[r.OriginalIndex]) } - view.SearchResults(galleries, 0).Render(w) + view.SearchResults(galleries).Render(w) }) log.Fatal(server.Serve(ln)) diff --git a/state/state.go b/state/state.go index d5f46c2..9c9369f 100644 --- a/state/state.go +++ b/state/state.go @@ -3,19 +3,19 @@ package state import ( "fmt" "goreader/gallery" - "maps" "slices" _ "golang.org/x/image/webp" ) type State struct { - Port string - Root string - CacheDir string - Galleries []gallery.Gallery - GalleryNames []string - UniqueTags map[gallery.Tag]int + Port string + Root string + CacheDir string + Galleries []gallery.Gallery + GalleryNames []string + // UniqueTags map[gallery.Tag]int + UniqueTags map[string]int UniqueArtists map[string]int UniqueGroups 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.GalleryNames = append(s.GalleryNames, g.Name()) 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() { - s.UniqueTags[tag]++ + s.UniqueTags[tag.Name]++ } for _, artist := range g.Artists() { s.UniqueArtists[artist]++ @@ -43,7 +43,6 @@ func (s *State) AddGallery(g gallery.Gallery) error { for _, parody := range g.Parodies() { s.UniqueParodies[parody]++ } - s.TagKeys = slices.Collect(maps.Keys(s.UniqueTags)) return nil } diff --git a/views/index.go b/views/index.go index eeee4af..34a2da9 100644 --- a/views/index.go +++ b/views/index.go @@ -6,6 +6,7 @@ import ( "goreader/gallery" "goreader/state" "strconv" + "strings" "github.com/silva-guimaraes/gtag" ) @@ -37,15 +38,22 @@ func tag(t gallery.Tag, state *state.State) *gtag.Tag { backgroundClass = "female" } d := gtag.New("a"). - Href("javascript:void(0)"). - Class("tag"). - SetAttr("hx-post", fmt.Sprintf("/filter/%s/%s", t.Name, t.Sex)). - SetAttr("hx-target", "#listing"). - SetAttr("hx-swap", "outerHTML") + Href(fmt.Sprintf("javascript:filter_tags('%s')", strings.TrimSpace(t.Name) /* FIXME */)). + Class("tag") { d.Tag("span").Text(gender).Class("name", backgroundClass) 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 } @@ -55,7 +63,8 @@ func thumbnail(src string) *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 { @@ -71,9 +80,21 @@ func InspectorInfo(g gallery.Gallery, state *state.State) *gtag.Tag { d.Tag("a"). SetAttr("href", fmt.Sprintf("/read/%s", g.Uuid())). SetAttr("hx-boost", "false"). - SetAttr("style", "color: white"). - Tag("h1").Text(g.Name()) - d.Tag("h2").Text(g.JpName()) + Tag("h1"). + SetAttr("style", "color: white; margin: 10px 0 10px 0;"). + 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() { 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 { + 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"). Class("cover"). Id(fmt.Sprintf("cover-%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("onmouseenter", "inspectSetTimeout(event)"). SetAttr("onmouseleave", "inspectClearTimeout(event)"). @@ -101,22 +128,19 @@ func cover(g gallery.Gallery) *gtag.Tag { return a } -const galleriesPerPage = 50 +const galleriesPerPage = 70 -func SearchResults(galleries []gallery.Gallery, page int) *gtag.Tag { - glm := max(len(galleries)-1, 0) - recent := galleries[min(page*galleriesPerPage, glm):min((page+1)*galleriesPerPage, glm)] - 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") +func SearchResults(galleries []gallery.Gallery) *gtag.Tag { + s := gtag.New("section") + results := s.Tag("section").Id("results") { - for _, g := range recent { - results.Append(cover(g)) + results.Tag("header").Tag("h1").Text(fmt.Sprintf("Listing %d Galleries", len(galleries))) + covers := results.Tag("main") + for _, g := range galleries { + covers.Append(cover(g)) } } - return m + return s } 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.Tag("a"). + Href("javascript:clear_filters()"). + Text("clear filters") nav.Div().Text("omakase v1") } center := body.Div().Id("center") { - top := center.Tag("div").Class("top") - { - top.Div().Id("search-bar").VoidTag("input"). - Class("ask-input"). - SetAttr("type", "search"). - 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)) - } + d := center.Div() + // HTMX faz com que isso receba [InspectorInfo] quando usuário paira o + // mouse sobre algum cover + d.Tag("section").Id("inspector") + d.Append(SearchResults(state.Galleries)) } body.Tag("footer").P().Text(randomQuote()) body.Tag("script").Asis(indexJavascript) diff --git a/views/static/index.js b/views/static/index.js index 9c29c3f..2b4303e 100644 --- a/views/static/index.js +++ b/views/static/index.js @@ -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} + */ + let covers = document.querySelectorAll('.cover'); + for (let cover of covers) { + /** + * @type {Array} + */ + 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} + */ + let covers = document.querySelectorAll('.cover'); + for (let cover of covers) { + /** + * @type {Array} + */ + 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 */ function inspectSetTimeout(event) { let target = event.target; - let id = setTimeout( () => { let targetId = '#' + target.id; @@ -33,7 +97,7 @@ function inspectSetTimeout(event) { 500 ); - target.dataset['timeout'] = id + target.dataset['timeout'] = id; } /** @@ -41,6 +105,6 @@ function inspectSetTimeout(event) { */ function inspectClearTimeout(event) { let target = event.target; - clearTimeout(target.dataset['timeout']) + clearTimeout(target.dataset['timeout']); } diff --git a/views/static/styles.css b/views/static/styles.css index 191e4dc..a7e4574 100644 --- a/views/static/styles.css +++ b/views/static/styles.css @@ -12,6 +12,12 @@ --main-margin: 62px; } +* { + margin: 0; + font-size-adjust: ex-height 0.53; + box-sizing: border-box; +} + .page { box-sizing: border-box; position: absolute; @@ -20,7 +26,7 @@ } .hidden { - display: none; + display: none !important; } #pages-container { @@ -94,12 +100,9 @@ body { #index nav { display: flex; - justify-content: flex-end; - /* margin-bottom: 11px; */ - /* background-color: var(--haughty-gray); */ + justify-content: space-between; border-bottom: 1px solid var(--silver-lining-white); padding: 5px; - /* box-shadow: black 1px 1px 1px 1px; */ } #index #content { @@ -116,14 +119,7 @@ body { padding: 20px; } -#index #center { - margin: var(--main-margin) var(--main-margin) auto var(--main-margin); -} - .top { - /* width: 100%; */ - /* display: flex; */ - /* flex-direction: row; */ display: flex; margin-bottom: 50px; } @@ -172,7 +168,7 @@ div#search-bar { } } -#index header { +#index > header { width: min-content; padding: 20px; font-family: sans-serif; @@ -233,10 +229,12 @@ footer { .tag, .caption { - font-family: Noto sans, sans-serif; - font-weight: 600; + font-family: sans-serif; + font-weight: 900; color: white; text-decoration-line: none; + margin: 2px; + border-radius: 3px 3px; } .tag { @@ -244,6 +242,7 @@ footer { color: var(--almost-white); text-decoration-line: none; padding: 0.13em; + background-color: var(--main-shadow); &>span:first-child { font-family: monospace; @@ -251,9 +250,6 @@ footer { border-radius: 3px 0 0 3px; } - &>span:nth-child(2) { - background-color: var(--haughty-gray); - } &>span:last-child { border-radius: 0 3px 3px 0; @@ -283,6 +279,7 @@ footer { .cover { display: inline-block; vertical-align: top; + width: 11%; } .caption { @@ -292,17 +289,21 @@ footer { } img { - filter: blur(15px); + filter: blur(30px); +} + +#index img { + width: 100%; } .thumbnail, .cover { - width: 148px; + /* width: 148px; */ height: auto; opacity: 0.93; } -#search-results { +#index #center { & #controls { width: calc(min-content / 2); height: min-content; @@ -325,9 +326,9 @@ img { } & #results { - display: grid; - width: 65%; + /* display: grid; */ grid-template-columns: repeat(5, 1fr); + width: 65%; @media (min-width: 1200px) { grid-template-columns: repeat(6, 1fr); @@ -349,9 +350,20 @@ img { Arial, 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-width: 100%; + object-fit: cover; /* width: auto; */ /* height: auto; */ } @@ -374,7 +386,7 @@ img { font-size: large; } - & div { + & div:last-child { margin-bottom: 100%; } }