diff --git a/api/devices_list.go b/api/devices_list.go index eba0dc6..d3bb44c 100644 --- a/api/devices_list.go +++ b/api/devices_list.go @@ -2,6 +2,7 @@ package api import ( "context" + "strconv" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/sqlite" @@ -12,24 +13,37 @@ import ( ) type DevicesListParams struct { - All bool - Q string + Q string + Status int + Limit int64 Offset int64 OrderBy 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]) { 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 { 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]) { - if dlp.Active { - expr = append(expr, models.SelectWhere.Devices.Enable.EQ(1)) + if dlp.Status > 0 { + expr = append(expr, models.SelectWhere.Devices.Enable.EQ(int32(dlp.Status-1))) } if dlp.Q != "" { diff --git a/server/routes/device_list.go b/server/routes/device_list.go index edf171d..31e8d0f 100644 --- a/server/routes/device_list.go +++ b/server/routes/device_list.go @@ -3,8 +3,6 @@ package routes import ( "encoding/json" "net/http" - "strconv" - "strings" "github.com/tigorlazuardi/redmage/api" "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") 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 { code, message := errs.HTTPMessage(err) 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") } } - -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 -} diff --git a/server/routes/page_devices.go b/server/routes/page_devices.go new file mode 100644 index 0000000..339251e --- /dev/null +++ b/server/routes/page_devices.go @@ -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") + } +} diff --git a/server/routes/routes.go b/server/routes/routes.go index 598be04..7e2f433 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -81,6 +81,7 @@ func (routes *Routes) registerWWWRoutes(router chi.Router) { r.Get("/subreddits/details/{name}", routes.PageSubredditsDetails) r.Get("/subreddits/add", routes.PageSubredditsAdd) r.Get("/config", routes.PageConfig) + r.Get("/devices", routes.PageDevices) r.Get("/schedules", routes.PageScheduleHistory) }) } diff --git a/views/devicesview/devicesview.templ b/views/devicesview/devicesview.templ new file mode 100644 index 0000000..48371e7 --- /dev/null +++ b/views/devicesview/devicesview.templ @@ -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) { +
+ @components.Container() { + if data.Error != "" { + @components.ErrorToast(data.Error) + } else { +

Devices

+
+

{ strconv.FormatInt(data.Total, 10) } Devices

+ @devicesList(data) + } + } +
+} + +templ devicesList(data Data) { +
+ for _, device := range data.Devices { + +
+
+

{ device.Name }

+ { device.Slug } +

{ fmt.Sprintf("%.0f \u00d7 %.0f", device.ResolutionX, device.ResolutionY) } px

+
+
+ if device.NSFW == 1 { +
NSFW
+ } +
Tolerance: { fmt.Sprintf("%.2f", device.AspectRatioTolerance) }
+ if device.MaxX > 0 { +
Max Width: { strconv.Itoa(int(device.MaxX)) }px
+ } + if device.MaxY > 0 { +
Max Height: { strconv.Itoa(int(device.MaxY)) }px
+ } + if device.MinX > 0 { +
Min Width: { strconv.Itoa(int(device.MinX)) }px
+ } + if device.MinY > 0 { +
Min Height: { strconv.Itoa(int(device.MinY)) }px
+ } +
+
+
+ } +
+} diff --git a/views/utils/cx.go b/views/utils/cx.go index 2e2313f..ec6c095 100644 --- a/views/utils/cx.go +++ b/views/utils/cx.go @@ -4,6 +4,8 @@ import "strings" // CX is a helper function to generate a string of class names based // 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 { b := strings.Builder{} for class, condition := range classes { @@ -15,3 +17,24 @@ func CX(classes map[string]bool) 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() +}