devices: added page
This commit is contained in:
parent
81031dc2aa
commit
b33b672cb6
|
@ -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
|
||||
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 != "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
36
server/routes/page_devices.go
Normal file
36
server/routes/page_devices.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
80
views/devicesview/devicesview.templ
Normal file
80
views/devicesview/devicesview.templ
Normal 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>
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue