devices: properly implemented and integrated windows wallpaper mode

This commit is contained in:
Tigor Hutasuhut 2024-05-07 22:50:23 +07:00
parent 0c8e6622bc
commit e8da8717ea
12 changed files with 150 additions and 6 deletions

View file

@ -262,6 +262,9 @@ func (post *Post) GetName() string {
} }
func (post *Post) GetImageTargetPath(cfg *config.Config, device *models.Device) string { func (post *Post) GetImageTargetPath(cfg *config.Config, device *models.Device) string {
if device.WindowsWallpaperMode == 1 {
return post.GetWindowsWallpaperImageTargetPath(cfg, device)
}
baseDownloadDir := cfg.String("download.directory") baseDownloadDir := cfg.String("download.directory")
p := path.Join(baseDownloadDir, device.Slug, post.GetSubreddit(), post.GetImageFilename()) p := path.Join(baseDownloadDir, device.Slug, post.GetSubreddit(), post.GetImageFilename())
abs, _ := filepath.Abs(p) abs, _ := filepath.Abs(p)
@ -269,6 +272,9 @@ func (post *Post) GetImageTargetPath(cfg *config.Config, device *models.Device)
} }
func (post *Post) GetImageTargetDir(cfg *config.Config, device *models.Device) string { func (post *Post) GetImageTargetDir(cfg *config.Config, device *models.Device) string {
if device.WindowsWallpaperMode == 1 {
return post.GetWindowsWallpaperImageTargetDir(cfg, device)
}
baseDownloadDir := cfg.String("download.directory") baseDownloadDir := cfg.String("download.directory")
p := path.Join(baseDownloadDir, device.Slug, post.GetSubreddit()) p := path.Join(baseDownloadDir, device.Slug, post.GetSubreddit())
abs, _ := filepath.Abs(p) abs, _ := filepath.Abs(p)
@ -316,6 +322,10 @@ func (post *Post) GetThumbnailRelativePath() string {
} }
func (post *Post) GetImageRelativePath(device *models.Device) string { func (post *Post) GetImageRelativePath(device *models.Device) string {
if device.WindowsWallpaperMode == 1 {
return post.GetWindowsWallpaperImageRelativePath(device)
}
return path.Join(device.Slug, post.GetSubreddit(), post.GetImageFilename()) return path.Join(device.Slug, post.GetSubreddit(), post.GetImageFilename())
} }

View file

@ -186,6 +186,10 @@ func (api *API) SubredditGetByNameWithImages(ctx context.Context, name string, i
return result, errs.Wrapw(err, "failed to get images by subreddit", "subreddit", result.Subreddit.Name, "params", imageParams) return result, errs.Wrapw(err, "failed to get images by subreddit", "subreddit", result.Subreddit.Name, "params", imageParams)
} }
if err := result.Images.LoadImageDevice(ctx, api.db); err != nil {
return result, errs.Wrapw(err, "failed to get device by images")
}
result.Total, err = models.Images. result.Total, err = models.Images.
Query(ctx, api.db, append(imageParams.CountQuery(), models.SelectWhere.Images.Subreddit.EQ(result.Subreddit.Name))...). Query(ctx, api.db, append(imageParams.CountQuery(), models.SelectWhere.Images.Subreddit.EQ(result.Subreddit.Name))...).
Count() Count()

View file

@ -6,11 +6,13 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"github.com/tigorlazuardi/redmage/models" "github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/errs"
"github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/pkg/telemetry" "github.com/tigorlazuardi/redmage/pkg/telemetry"
"github.com/tigorlazuardi/redmage/views/components"
) )
func (routes *Routes) APIDeviceCreate(rw http.ResponseWriter, r *http.Request) { func (routes *Routes) APIDeviceCreate(rw http.ResponseWriter, r *http.Request) {
@ -95,3 +97,67 @@ func validateCreateDevice(params *models.Device) error {
} }
return nil return nil
} }
func (routes *Routes) DevicesCreateHTMX(rw http.ResponseWriter, req *http.Request) {
var err error
ctx, span := tracer.Start(req.Context(), "*Routes.DevicesCreateHTMX")
defer func() { telemetry.EndWithStatus(span, err) }()
device, err := createDeviceFromParams(req)
if err != nil {
rw.WriteHeader(400)
if err := components.ErrorToast(err.Error()).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error notification")
}
return
}
_, err = routes.API.DevicesCreate(ctx, device)
if err != nil {
log.New(ctx).Err(err).Error("failed to create device", "device", device)
code, message := errs.HTTPMessage(err)
rw.WriteHeader(code)
if err := components.ErrorToast(message).Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render error notification")
}
return
}
rw.Header().Set("HX-Redirect", "/devices")
rw.WriteHeader(http.StatusCreated)
if err := components.SuccessToast("device created").Render(ctx, rw); err != nil {
log.New(ctx).Err(err).Error("failed to render success notification")
}
}
func createDeviceFromParams(req *http.Request) (*models.Device, error) {
device := new(models.Device)
device.Enable = 1
device.Name = req.FormValue("name")
device.Slug = req.FormValue("slug")
device.ResolutionX, _ = strconv.ParseFloat(req.FormValue("resolution_x"), 32)
device.ResolutionY, _ = strconv.ParseFloat(req.FormValue("resolution_y"), 32)
device.AspectRatioTolerance, _ = strconv.ParseFloat(req.FormValue("aspect_ratio_tolerance"), 32)
maxX, _ := strconv.ParseInt(req.FormValue("max_x"), 10, 32)
device.MaxX = int32(maxX)
maxY, _ := strconv.ParseInt(req.FormValue("max_y"), 10, 32)
device.MaxY = int32(maxY)
minX, _ := strconv.ParseInt(req.FormValue("min_x"), 10, 32)
device.MinX = int32(minX)
minY, _ := strconv.ParseInt(req.FormValue("min_y"), 10, 32)
device.MinY = int32(minY)
nsfw, _ := strconv.ParseInt(req.FormValue("nsfw"), 10, 32)
device.NSFW = int32(nsfw)
windowsWallpaperMode, _ := strconv.ParseInt(req.FormValue("windows_wallpaper_mode"), 10, 32)
device.WindowsWallpaperMode = int32(windowsWallpaperMode)
return device, validateCreateDevice(device)
}

View file

@ -40,7 +40,7 @@ func (routes *Routes) PageSubredditsDetails(rw http.ResponseWriter, r *http.Requ
data.Subreddit = result.Subreddit data.Subreddit = result.Subreddit
data.Images = result.Images data.Images = result.Images
data.TotalImages = result.Total data.TotalImages = result.Total
data.Devices, err = routes.API.GetDevices(ctx, api.DevicesListParams{}) data.Devices, err = routes.API.GetDevices(ctx, api.DevicesListParams{Status: -1})
if err != nil { if err != nil {
log.New(ctx).Err(err).Error("failed to get devices") log.New(ctx).Err(err).Error("failed to get devices")
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)

View file

@ -63,6 +63,7 @@ func (routes *Routes) registerHTMXRoutes(router chi.Router) {
router.Post("/subreddits/check", routes.SubredditCheckHTMX) router.Post("/subreddits/check", routes.SubredditCheckHTMX)
router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX) router.Get("/subreddits/validate/schedule", routes.SubredditValidateScheduleHTMX)
router.Post("/devices/add", routes.DevicesCreateHTMX)
router.Post("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX) router.Post("/devices/add/validate/slug", routes.DevicesValidateSlugHTMX)
router.Post("/devices/add/validate/name", routes.DevicesValidateNameHTMX) router.Post("/devices/add/validate/name", routes.DevicesValidateNameHTMX)
} }

View file

@ -14,6 +14,7 @@ const (
HideTitle ImageCardOption = 1 << iota HideTitle ImageCardOption = 1 << iota
HideSubreddit HideSubreddit
HidePoster HidePoster
HideDevice
) )
templ ImageCard(data *models.Image, opts ImageCardOption) { templ ImageCard(data *models.Image, opts ImageCardOption) {
@ -48,6 +49,11 @@ templ ImageCard(data *models.Image, opts ImageCardOption) {
<p class="text-xs">{ fmt.Sprintf("%d \u00d7 %d", data.ImageWidth, data.ImageHeight) } px</p> <p class="text-xs">{ fmt.Sprintf("%d \u00d7 %d", data.ImageWidth, data.ImageHeight) } px</p>
<p class="text-xs text-end">{ formatByteSize(data.ImageSize) }</p> <p class="text-xs text-end">{ formatByteSize(data.ImageSize) }</p>
</div> </div>
if data.R.Device != nil && !opts.Has(HideDevice) {
<a href={ templ.URL(fmt.Sprintf("/devices/details/%s", data.R.Device.Slug)) }>
<div class="divider text-accent underline">{ data.R.Device.Name }</div>
</a>
}
</div> </div>
</div> </div>
} }

View file

@ -9,7 +9,7 @@ type NameInputData struct {
} }
templ NameInput(data NameInputData) { templ NameInput(data NameInputData) {
<label class="form-control"> <label id="name-input-form" class="form-control">
<div class="label"> <div class="label">
<span <span
class={ utils.CXX("label-text", true, "text-error", data.Error != "") } class={ utils.CXX("label-text", true, "text-error", data.Error != "") }
@ -21,6 +21,8 @@ templ NameInput(data NameInputData) {
hx-post="/htmx/devices/add/validate/name" hx-post="/htmx/devices/add/validate/name"
hx-include="[name='slug']" hx-include="[name='slug']"
hx-trigger="change" hx-trigger="change"
hx-target="#name-input-form"
hx-swap="outerHTML"
name="name" name="name"
type="text" type="text"
class={ utils.CXX("input input-bordered", true, "input-error", data.Error != "") } class={ utils.CXX("input input-bordered", true, "input-error", data.Error != "") }

View file

@ -0,0 +1,24 @@
package adddevice
type NSFWCheckboxData struct {
Checked bool
}
templ NSFWCheckbox(data NSFWCheckboxData) {
<div class="form-control">
<div class="divider my-0 py-0"></div>
<div class="divider my-0 py-0"></div>
<label class="label cursor-pointer">
<span class="label-text">NSFW</span>
<input
type="checkbox"
checked?={ data.Checked }
class="checkbox"
name="nsfw"
value="1"
/>
</label>
<div class="divider my-0 py-0"></div>
<span class="label-text pl-1">Whether to allow NSFW images for current device.</span>
</div>
}

View file

@ -22,6 +22,9 @@ templ Content(c *views.Context) {
hx-post="/htmx/devices/add" hx-post="/htmx/devices/add"
action="/htmx/devices/add" action="/htmx/devices/add"
class="grid sm:grid-cols-2 gap-4" class="grid sm:grid-cols-2 gap-4"
hx-target={ components.NotificationContainerID }
hx-target-error={ components.NotificationContainerID }
hx-swap="afterbegin"
> >
@NameInput(NameInputData{}) @NameInput(NameInputData{})
@SlugInput(SlugInputData{}) @SlugInput(SlugInputData{})
@ -29,6 +32,8 @@ templ Content(c *views.Context) {
@ResolutionYInput(ResolutionData{}) @ResolutionYInput(ResolutionData{})
<div class="divider my-auto sm:col-span-2"><h3 class="m-0 p-0">Filter</h3></div> <div class="divider my-auto sm:col-span-2"><h3 class="m-0 p-0">Filter</h3></div>
@AspectRatioToleranceInput(AspectRatioToleranceData{Value: 0.2}) @AspectRatioToleranceInput(AspectRatioToleranceData{Value: 0.2})
@NSFWCheckbox(NSFWCheckboxData{Checked: true})
@WindowsWallpaperCheckbox(WindowsWallpaperCheckboxData{})
@MinImageResolutionXInput(ResolutionData{}) @MinImageResolutionXInput(ResolutionData{})
@MinImageResolutionYInput(ResolutionData{}) @MinImageResolutionYInput(ResolutionData{})
@MaxImageResolutionXInput(ResolutionData{}) @MaxImageResolutionXInput(ResolutionData{})
@ -37,4 +42,5 @@ templ Content(c *views.Context) {
</form> </form>
} }
</main> </main>
@components.NotificationContainer()
} }

View file

@ -0,0 +1,27 @@
package adddevice
type WindowsWallpaperCheckboxData struct {
Checked bool
}
templ WindowsWallpaperCheckbox(data WindowsWallpaperCheckboxData) {
<div class="form-control">
<div class="divider my-0 py-0"></div>
<div class="divider my-0 py-0"></div>
<label class="label cursor-pointer">
<span class="label-text">Windows Wallpaper Mode</span>
<input
type="checkbox"
checked?={ data.Checked }
class="checkbox"
name="windows_wallpaper_mode"
value="1"
/>
</label>
<div class="divider my-0 py-0"></div>
<span class="label-text pl-1">
Windows Wallpaper Mode puts images under one folder instead of split by subreddits.
This allows the user to target Windows Wallpaper to the whole image collections.
</span>
</div>
}

View file

@ -137,7 +137,7 @@ templ nsfwToggle(c *views.Context, data Data) {
templ RecentlyAddedImageList(images models.ImageSlice, opts components.ImageCardOption) { templ RecentlyAddedImageList(images models.ImageSlice, opts components.ImageCardOption) {
<div class="overflow-x-auto flex gap-4 p-6 shadow-inner bg-base-300 rounded-2xl w-[85vw] md:w-full scrollbar-track-base-100 scrollbar-thumb-primary scrollbar-thin hover:scrollbar-thumb-base-300"> <div class="overflow-x-auto flex gap-4 p-6 shadow-inner bg-base-300 rounded-2xl w-[85vw] md:w-full scrollbar-track-base-100 scrollbar-thumb-primary scrollbar-thin hover:scrollbar-thumb-base-300">
for _, data := range images { for _, data := range images {
@components.ImageCard(data, 0) @components.ImageCard(data, components.HideDevice)
} }
</div> </div>
} }

View file

@ -114,10 +114,8 @@ templ FilterBar(c *views.Context, data Data) {
<select name="device" class="select select-bordered w-full"> <select name="device" class="select select-bordered w-full">
if len(data.Devices) == 0 { if len(data.Devices) == 0 {
<option disabled selected>No Devices</option> <option disabled selected>No Devices</option>
} else if data.Params.Device == "" {
<option value="" selected>*No Filter</option>
} else { } else {
<option value="">*No Filter</option> <option value="" selected?={ data.Params.Device == "" }>*No Filter</option>
} }
for _, device := range data.Devices { for _, device := range data.Devices {
@deviceOption(data.Params, device) @deviceOption(data.Params, device)