devices: added page

This commit is contained in:
Tigor Hutasuhut 2024-05-06 18:50:43 +07:00
parent 81031dc2aa
commit b33b672cb6
6 changed files with 169 additions and 35 deletions

View file

@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"strconv"
"github.com/stephenafamo/bob" "github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite" "github.com/stephenafamo/bob/dialect/sqlite"
@ -12,24 +13,37 @@ import (
) )
type DevicesListParams struct { type DevicesListParams struct {
All bool
Q string Q string
Status int
Limit int64 Limit int64
Offset int64 Offset int64
OrderBy string OrderBy string
Sort string Sort string
Active bool }
func (dlp *DevicesListParams) FillFromQuery(query Queryable) {
dlp.Status, _ = strconv.Atoi(query.Get("status"))
if dlp.Status > 2 {
dlp.Status = 2
}
dlp.Q = query.Get("q")
dlp.Limit, _ = strconv.ParseInt(query.Get("limit"), 10, 64)
if dlp.Limit < 1 {
dlp.Limit = 20
}
dlp.Offset, _ = strconv.ParseInt(query.Get("offset"), 10, 64)
if dlp.Offset < 0 {
dlp.Offset = 0
}
dlp.OrderBy = query.Get("order_by")
dlp.Sort = query.Get("sort")
} }
func (dlp DevicesListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) { func (dlp DevicesListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
expr = append(expr, dlp.CountQuery()...) expr = append(expr, dlp.CountQuery()...)
if dlp.Active {
expr = append(expr, models.SelectWhere.Devices.Enable.EQ(1))
}
if dlp.All {
return expr
}
if dlp.Limit > 0 { if dlp.Limit > 0 {
expr = append(expr, sm.Limit(dlp.Limit)) expr = append(expr, sm.Limit(dlp.Limit))
@ -54,8 +68,8 @@ func (dlp DevicesListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
} }
func (dlp DevicesListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) { func (dlp DevicesListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
if dlp.Active { if dlp.Status > 0 {
expr = append(expr, models.SelectWhere.Devices.Enable.EQ(1)) expr = append(expr, models.SelectWhere.Devices.Enable.EQ(int32(dlp.Status-1)))
} }
if dlp.Q != "" { if dlp.Q != "" {

View file

@ -3,8 +3,6 @@ package routes
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"strings"
"github.com/tigorlazuardi/redmage/api" "github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/errs"
@ -15,9 +13,10 @@ func (routes *Routes) APIDeviceList(rw http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "*Routes.APIDeviceList") ctx, span := tracer.Start(r.Context(), "*Routes.APIDeviceList")
defer span.End() defer span.End()
query := parseApiDeviceListQueries(r) var params api.DevicesListParams
params.FillFromQuery(r.URL.Query())
result, err := routes.API.DevicesList(ctx, query) result, err := routes.API.DevicesList(ctx, params)
if err != nil { if err != nil {
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)
rw.WriteHeader(code) rw.WriteHeader(code)
@ -29,22 +28,3 @@ func (routes *Routes) APIDeviceList(rw http.ResponseWriter, r *http.Request) {
log.New(ctx).Err(err).Error("failed to marshal json api devices") log.New(ctx).Err(err).Error("failed to marshal json api devices")
} }
} }
func parseApiDeviceListQueries(req *http.Request) (params api.DevicesListParams) {
params.All, _ = strconv.ParseBool(req.FormValue("all"))
params.Offset, _ = strconv.ParseInt(req.FormValue("offset"), 10, 64)
params.Limit, _ = strconv.ParseInt(req.FormValue("limit"), 10, 64)
params.Q = req.FormValue("q")
params.OrderBy = req.FormValue("order")
params.Sort = strings.ToLower(req.FormValue("sort"))
if params.Limit < 1 {
params.Limit = 10
}
if params.OrderBy == "" {
params.OrderBy = "name"
}
return params
}

View file

@ -0,0 +1,36 @@
package routes
import (
"net/http"
"github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/views"
"github.com/tigorlazuardi/redmage/views/devicesview"
)
func (routes *Routes) PageDevices(rw http.ResponseWriter, req *http.Request) {
ctx, start := tracer.Start(req.Context(), "*Routes.PageDevices")
defer start.End()
vc := views.NewContext(routes.Config, req)
var data devicesview.Data
data.Params.FillFromQuery(req.URL.Query())
result, err := routes.API.DevicesList(ctx, data.Params)
if err != nil {
log.New(ctx).Err(err).Error("failed to query devices")
code, message := errs.HTTPMessage(err)
rw.WriteHeader(code)
data.Error = message
if err := devicesview.Devices(vc, data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render devices error view")
}
}
data.Devices = result.Devices
data.Total = result.Total
if err := devicesview.Devices(vc, data).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render devices view")
}
}

View file

@ -81,6 +81,7 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) {
r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails) r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails)
r.Get("/subreddits/add", routes.PageSubredditsAdd) r.Get("/subreddits/add", routes.PageSubredditsAdd)
r.Get("/config", routes.PageConfig) r.Get("/config", routes.PageConfig)
r.Get("/devices", routes.PageDevices)
r.Get("/schedules", routes.PageScheduleHistory) r.Get("/schedules", routes.PageScheduleHistory)
}) })
} }

View file

@ -0,0 +1,80 @@
package devicesview
import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/components"
import "github.com/tigorlazuardi/redmage/models"
import "github.com/tigorlazuardi/redmage/api"
import "strconv"
import "fmt"
import "github.com/tigorlazuardi/redmage/views/utils"
type Data struct {
Error string
Devices models.DeviceSlice
Total int64
Params api.DevicesListParams
}
templ Devices(c *views.Context, data Data) {
@components.Doctype() {
@components.Head(c, components.HeadTitle("Redmage - Devices"))
@components.Body(c) {
@DevicesContent(c, data)
}
}
}
templ DevicesContent(c *views.Context, data Data) {
<main class="prose min-w-full">
@components.Container() {
if data.Error != "" {
@components.ErrorToast(data.Error)
} else {
<h1>Devices</h1>
<div class="divider"></div>
<h2>{ strconv.FormatInt(data.Total, 10) } Devices</h2>
@devicesList(data)
}
}
</main>
}
templ devicesList(data Data) {
<div class="grid md:grid-cols-2 gap-4">
for _, device := range data.Devices {
<a
href={ templ.URL(fmt.Sprintf("/devices/details/%s", device.Slug)) }
class={ utils.CXX(
"card bg-base-100 no-underline text-primary hover:bg-base-200 shadow-xl rounded-xl top-0 hover:-top-1 transition-all", true,
"bg-base-300 hover:bg-base-300", device.Enable == 0,
) }
>
<div class="card-body">
<div class="card-title">
<h2 class="my-auto">{ device.Name }</h2>
<span class="text-sm self-end italic font-normal">{ device.Slug }</span>
<p class="text-xs my-auto text-end">{ fmt.Sprintf("%.0f \u00d7 %.0f", device.ResolutionX, device.ResolutionY) } px</p>
</div>
<div class="flex flex-wrap gap-4">
if device.NSFW == 1 {
<div class="badge badge-accent">NSFW</div>
}
<div class="badge badge-secondary">Tolerance: { fmt.Sprintf("%.2f", device.AspectRatioTolerance) }</div>
if device.MaxX > 0 {
<div class="badge badge-primary">Max Width: { strconv.Itoa(int(device.MaxX)) }px</div>
}
if device.MaxY > 0 {
<div class="badge badge-primary">Max Height: { strconv.Itoa(int(device.MaxY)) }px</div>
}
if device.MinX > 0 {
<div class="badge badge-primary">Min Width: { strconv.Itoa(int(device.MinX)) }px</div>
}
if device.MinY > 0 {
<div class="badge badge-primary">Min Height: { strconv.Itoa(int(device.MinY)) }px</div>
}
</div>
</div>
</a>
}
</div>
}

View file

@ -4,6 +4,8 @@ import "strings"
// CX is a helper function to generate a string of class names based // CX is a helper function to generate a string of class names based
// on a map of class names and their conditions. // on a map of class names and their conditions.
//
// CX is not guaranteed to be ordered, use CXX for that.
func CX(classes map[string]bool) string { func CX(classes map[string]bool) string {
b := strings.Builder{} b := strings.Builder{}
for class, condition := range classes { for class, condition := range classes {
@ -15,3 +17,24 @@ func CX(classes map[string]bool) string {
return strings.TrimSpace(b.String()) return strings.TrimSpace(b.String())
} }
// CXX takes an alternating string and boolean arguments.
// Odd values must be string and even values must be boolean.
//
// Function panics if above conditions are not fulfilled.
//
// Example:
//
// utils.CXX("my-0", true, "my-1", cond)
func CXX(classAndConds ...any) string {
s := strings.Builder{}
for i, j := 0, 1; j < len(classAndConds); i, j = i+2, j+2 {
class := classAndConds[i].(string)
b := classAndConds[j].(bool)
if b {
s.WriteString(class)
s.WriteByte(' ')
}
}
return s.String()
}