feat: add sfw checkbox in browse

This commit is contained in:
2026-05-06 13:31:28 +02:00
parent b2741a4525
commit 72d19a7f63
4 changed files with 44 additions and 23 deletions

View File

@@ -141,6 +141,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
orderBy := r.URL.Query().Get("order_by")
sort := r.URL.Query().Get("sort")
sfw := r.URL.Query().Get("sfw") != "false"
var genres []int
for _, g := range r.URL.Query()["genres"] {
@@ -155,7 +156,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
res, err := h.service.jikanClient.SearchAdvanced(ctx, q, animeType, status, orderBy, sort, genres, page, 24)
res, err := h.service.jikanClient.SearchAdvanced(ctx, q, animeType, status, orderBy, sort, genres, sfw, page, 24)
if err != nil {
if errors.Is(err, context.Canceled) {
return
@@ -183,6 +184,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) {
"OrderBy": orderBy,
"Sort": sort,
"Genres": genres,
"SFW": sfw,
"WatchlistMap": watchlistMap,
})
if err != nil {
@@ -220,6 +222,7 @@ func (h *Handler) HandleBrowse(w http.ResponseWriter, r *http.Request) {
"OrderBy": orderBy,
"Sort": sort,
"Genres": genres,
"SFW": sfw,
"GenresList": genresList,
"Animes": res.Animes,
"HasNextPage": res.HasNextPage,
@@ -398,7 +401,7 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]quickSearchResult{})
return
}
res, err := h.service.jikanClient.SearchAdvanced(r.Context(), query, "", "", "", "", nil, 1, 5)
res, err := h.service.jikanClient.SearchAdvanced(r.Context(), query, "", "", "", "", nil, true, 1, 5)
if err != nil {
log.Printf("quick search error: %v", err)
w.WriteHeader(http.StatusOK)

View File

@@ -12,7 +12,7 @@ func (c *Client) Search(ctx context.Context, query string, page int) (SearchResu
return c.search(ctx, query, page, 0)
}
func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, orderBy, sort string, genres []int, page, limit int) (SearchResult, error) {
func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (SearchResult, error) {
if page < 1 {
page = 1
}
@@ -29,10 +29,13 @@ func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, o
genresParam = strings.Join(ids, ",")
}
cacheKey := fmt.Sprintf("search:%s:%s:%s:%s:%s:%s:%d:%d", query, animeType, status, orderBy, sort, genresParam, page, limit)
cacheKey := fmt.Sprintf("search:%s:%s:%s:%s:%s:%s:%v:%d:%d", query, animeType, status, orderBy, sort, genresParam, sfw, page, limit)
var result SearchResponse
reqURL := fmt.Sprintf("%s/anime?page=%d", c.baseURL, page)
if sfw {
reqURL += "&sfw=true"
}
if query != "" {
reqURL += "&q=" + url.QueryEscape(query)
}

View File

@@ -23,7 +23,7 @@
{{range $i, $anime := .Animes}}
{{$isThreshold := eq (add $i 1) (sub (len $.Animes) 8)}}
{{if and $isThreshold $.HasNextPage}}
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&sfw={{$.SFW}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
hx-trigger="revealed"
hx-swap="afterend"
hx-target="this"
@@ -45,7 +45,7 @@
{{range $i, $anime := .Animes}}
{{$isThreshold := eq (add $i 1) (sub $count 8)}}
{{if and $isThreshold $.HasNextPage}}
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&sfw={{$.SFW}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
hx-trigger="revealed"
hx-swap="afterend"
hx-target="this"

View File

@@ -17,10 +17,24 @@
{{if .Status}}<input type="hidden" name="status" value="{{.Status}}">{{end}}
{{if .OrderBy}}<input type="hidden" name="order_by" value="{{.OrderBy}}">{{end}}
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
{{if not .SFW}}<input type="hidden" name="sfw" value="false">{{end}}
{{range $g := .Genres}}<input type="hidden" name="genres" value="{{$g}}">{{end}}
</form>
</div>
<div class="flex items-center gap-2 px-3 py-2 text-sm text-white bg-black/20">
<label class="flex cursor-pointer items-center gap-2">
<div class="flex h-4 w-4 items-center justify-center border-2 transition-colors {{if .SFW}}border-accent bg-accent{{else}}border-zinc-500 bg-transparent{{end}}">
{{if .SFW}}
<svg class="h-3 w-3 text-black" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6 9 17l-5-5" /></svg>
{{end}}
</div>
<input type="checkbox" class="sr-only" name="sfw" value="true" {{if .SFW}}checked{{end}}
onchange="const params = new URLSearchParams(window.location.search); params.set('sfw', this.checked); window.location.search = params.toString();">
SFW
</label>
</div>
<ui-dropdown class="relative block" data-align="left" data-width="w-48">
<div data-trigger class="cursor-pointer">
<button class="flex items-center gap-2 bg-black/20 px-3 py-2 text-sm text-white hover:bg-black/30">
@@ -35,6 +49,7 @@
{{if .Status}}<input type="hidden" name="status" value="{{.Status}}">{{end}}
{{if .OrderBy}}<input type="hidden" name="order_by" value="{{.OrderBy}}">{{end}}
{{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}">{{end}}
{{if not .SFW}}<input type="hidden" name="sfw" value="false">{{end}}
{{range .GenresList}}
{{$genreID := .MalID}}
{{$isSelected := hasGenre $genreID $selectedGenreIDs}}
@@ -61,10 +76,10 @@
</div>
<div data-content class="hidden absolute z-50 w-40 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?status=&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Status</a>
<a href="?status=airing&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Airing</a>
<a href="?status=complete&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Complete</a>
<a href="?status=upcoming&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Upcoming</a>
<a href="?status=&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Status</a>
<a href="?status=airing&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Airing</a>
<a href="?status=complete&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Complete</a>
<a href="?status=upcoming&q={{.Query}}&type={{.Type}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Upcoming</a>
</div>
</div>
</ui-dropdown>
@@ -78,12 +93,12 @@
</div>
<div data-content class="hidden absolute z-50 w-40 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?type=&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Type</a>
<a href="?type=tv&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">TV</a>
<a href="?type=movie&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Movie</a>
<a href="?type=ova&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">OVA</a>
<a href="?type=special&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Special</a>
<a href="?type=ona&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">ONA</a>
<a href="?type=&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Any Type</a>
<a href="?type=tv&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">TV</a>
<a href="?type=movie&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Movie</a>
<a href="?type=ova&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">OVA</a>
<a href="?type=special&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Special</a>
<a href="?type=ona&q={{.Query}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">ONA</a>
</div>
</div>
</ui-dropdown>
@@ -98,17 +113,17 @@
</div>
<div data-content class="hidden absolute z-50 w-48 bg-background-button rounded-none shadow-2xl left-0 top-full mt-2">
<div class="flex flex-col py-1">
<a href="?order_by=&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Default</a>
<a href="?order_by=popularity&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Popularity</a>
<a href="?order_by=score&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Score</a>
<a href="?order_by=title&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Title</a>
<a href="?order_by=start_date&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Start Date</a>
<a href="?order_by=episodes&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Episodes</a>
<a href="?order_by=&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Default</a>
<a href="?order_by=popularity&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Popularity</a>
<a href="?order_by=score&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Score</a>
<a href="?order_by=title&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Title</a>
<a href="?order_by=start_date&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Start Date</a>
<a href="?order_by=episodes&q={{.Query}}&status={{.Status}}&type={{.Type}}&sort={{.Sort}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10 text-sm text-white">Episodes</a>
</div>
</div>
</ui-dropdown>
<a href="?sort={{if eq .Sort "asc"}}desc{{else}}asc{{end}}&q={{.Query}}&status={{.Status}}&type={{.Type}}&order_by={{.OrderBy}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex h-9 w-9 items-center justify-center bg-black/20 text-neutral-300 hover:text-white">
<a href="?sort={{if eq .Sort "asc"}}desc{{else}}asc{{end}}&q={{.Query}}&status={{.Status}}&type={{.Type}}&order_by={{.OrderBy}}&sfw={{.SFW}}{{ if .Genres }}&{{ genresParams .Genres }}{{ end }}" class="flex h-9 w-9 items-center justify-center bg-black/20 text-neutral-300 hover:text-white">
{{if eq .Sort "asc"}}
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
{{else}}