sqlite: date based columns migrated to int64

This commit is contained in:
Tigor Hutasuhut 2024-04-29 21:45:18 +07:00
parent 6c973cdf1f
commit 2b4df20754
21 changed files with 168 additions and 137 deletions

View file

@ -47,6 +47,16 @@ build-dependencies:
echo "Dayjs Relative Time not found, installing it" echo "Dayjs Relative Time not found, installing it"
curl -o public/dayjs-relativeTime-1.11.10.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/plugin/relativeTime.min.js curl -o public/dayjs-relativeTime-1.11.10.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/plugin/relativeTime.min.js
fi fi
@if [ ! -f "public/dayjs-utc-1.11.10.min.js" ]; then
mkdir -p public
echo "Dayjs UTC plugin not found, installing it"
curl -o public/dayjs-utc-1.11.10.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/plugin/utc.min.js
fi
@if [ ! -f "public/dayjs-timezone-1.11.10.min.js" ]; then
mkdir -p public
echo "Dayjs Timezone plugin not found, installing it"
curl -o public/dayjs-timezone-1.11.10.min.js https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/plugin/timezone.min.js
fi
@if [ ! -f "public/theme-change-2.0.2.min.js" ]; then @if [ ! -f "public/theme-change-2.0.2.min.js" ]; then
mkdir -p public mkdir -p public
echo "Theme change not found, installing it" echo "Theme change not found, installing it"
@ -71,5 +81,8 @@ migrate-new:
migrate-redo: migrate-redo:
@goose redo @goose redo
migrate-down:
@goose down
migrate-up: migrate-up:
@goose up @goose up

View file

@ -3,7 +3,9 @@ package api
import ( import (
"context" "context"
"errors" "errors"
"time"
"github.com/aarondl/opt/omit"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"github.com/tigorlazuardi/redmage/models" "github.com/tigorlazuardi/redmage/models"
"github.com/tigorlazuardi/redmage/pkg/errs" "github.com/tigorlazuardi/redmage/pkg/errs"
@ -11,11 +13,27 @@ import (
type DeviceCreateParams = models.DeviceSetter type DeviceCreateParams = models.DeviceSetter
func (api *API) DevicesCreate(ctx context.Context, params *DeviceCreateParams) (*models.Device, error) { func (api *API) DevicesCreate(ctx context.Context, params *models.Device) (*models.Device, error) {
ctx, span := tracer.Start(ctx, "*API.DevicesCreate") ctx, span := tracer.Start(ctx, "*API.DevicesCreate")
defer span.End() defer span.End()
device, err := models.Devices.Insert(ctx, api.db, params) now := time.Now()
device, err := models.Devices.Insert(ctx, api.db, &models.DeviceSetter{
Slug: omit.From(params.Slug),
Name: omit.From(params.Name),
ResolutionX: omit.From(params.ResolutionX),
ResolutionY: omit.From(params.ResolutionY),
AspectRatioTolerance: omit.From(params.AspectRatioTolerance),
MinX: omit.From(params.MinX),
MinY: omit.From(params.MinY),
MaxX: omit.From(params.MaxX),
MaxY: omit.From(params.MaxY),
NSFW: omit.From(params.NSFW),
WindowsWallpaperMode: omit.From(params.WindowsWallpaperMode),
Enable: omit.From(params.Enable),
CreatedAt: omit.From(now.Unix()),
UpdatedAt: omit.From(now.Unix()),
})
if err != nil { if err != nil {
var sqliteErr sqlite3.Error var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) { if errors.As(err, &sqliteErr) {

View file

@ -186,6 +186,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su
} }
var many []*models.ImageSetter var many []*models.ImageSetter
now := time.Now()
for _, device := range devices { for _, device := range devices {
var nsfw int32 var nsfw int32
if post.IsNSFW() { if post.IsNSFW() {
@ -197,7 +198,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su
Title: omit.From(post.GetTitle()), Title: omit.From(post.GetTitle()),
PostID: omit.From(post.GetID()), PostID: omit.From(post.GetID()),
PostURL: omit.From(post.GetPostURL()), PostURL: omit.From(post.GetPostURL()),
PostCreated: omit.From(post.GetCreated().Format(time.RFC3339)), PostCreated: omit.From(post.GetCreated().Unix()),
PostName: omit.From(post.GetName()), PostName: omit.From(post.GetName()),
Poster: omit.From(post.GetAuthor()), Poster: omit.From(post.GetAuthor()),
PosterURL: omit.From(post.GetAuthorURL()), PosterURL: omit.From(post.GetAuthorURL()),
@ -206,6 +207,8 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su
ImageOriginalURL: omit.From(post.GetImageURL()), ImageOriginalURL: omit.From(post.GetImageURL()),
ThumbnailOriginalURL: omit.From(post.GetThumbnailURL()), ThumbnailOriginalURL: omit.From(post.GetThumbnailURL()),
NSFW: omit.From(nsfw), NSFW: omit.From(nsfw),
CreatedAt: omit.From(now.Unix()),
UpdatedAt: omit.From(now.Unix()),
}) })
} }

View file

@ -48,6 +48,9 @@ func (ilp *ImageListParams) FillFromQuery(query Queryable) {
createdAtint, _ := strconv.ParseInt(query.Get("created_at"), 10, 64) createdAtint, _ := strconv.ParseInt(query.Get("created_at"), 10, 64)
if createdAtint > 0 { if createdAtint > 0 {
ilp.CreatedAt = time.Unix(createdAtint, 0) ilp.CreatedAt = time.Unix(createdAtint, 0)
} else if createdAtint < 0 {
// Negative value means relative to now.
ilp.CreatedAt = time.Now().Add(time.Duration(createdAtint) * time.Second)
} }
} }
@ -76,7 +79,7 @@ func (ilp ImageListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
} }
if !ilp.CreatedAt.IsZero() { if !ilp.CreatedAt.IsZero() {
expr = append(expr, models.SelectWhere.Images.CreatedAt.GTE(ilp.CreatedAt.Format(time.RFC3339))) expr = append(expr, models.SelectWhere.Images.CreatedAt.GTE(ilp.CreatedAt.Unix()))
} }
return expr return expr

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"time"
"github.com/aarondl/opt/omit" "github.com/aarondl/opt/omit"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
@ -15,12 +16,15 @@ func (api *API) SubredditsCreate(ctx context.Context, params *models.Subreddit)
ctx, span := tracer.Start(ctx, "*API.SubredditsCreate") ctx, span := tracer.Start(ctx, "*API.SubredditsCreate")
defer span.End() defer span.End()
now := time.Now()
set := &models.SubredditSetter{ set := &models.SubredditSetter{
Name: omit.From(params.Name), Name: omit.From(params.Name),
EnableSchedule: omit.From(params.EnableSchedule), EnableSchedule: omit.From(params.EnableSchedule),
Subtype: omit.From(params.Subtype), Subtype: omit.From(params.Subtype),
Schedule: omit.From(params.Schedule), Schedule: omit.From(params.Schedule),
Countback: omit.From(params.Countback), Countback: omit.From(params.Countback),
CreatedAt: omit.From(now.Unix()),
UpdatedAt: omit.From(now.Unix()),
} }
subreddit, err = models.Subreddits.Insert(ctx, api.db, set) subreddit, err = models.Subreddits.Insert(ctx, api.db, set)

View file

@ -5,10 +5,10 @@ CREATE TABLE subreddits (
name VARCHAR(30) NOT NULL, name VARCHAR(30) NOT NULL,
enable_schedule INT NOT NULL DEFAULT 1, enable_schedule INT NOT NULL DEFAULT 1,
subtype INT NOT NULL DEFAULT 0, subtype INT NOT NULL DEFAULT 0,
schedule VARCHAR(20) NOT NULL DEFAULT '0 0 * * *', schedule VARCHAR(20) NOT NULL DEFAULT '@daily',
countback INT NOT NULL DEFAULT 100, countback INT NOT NULL DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at BIGINT DEFAULT 0 NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL updated_at BIGINT DEFAULT 0 NOT NULL
); );
CREATE UNIQUE INDEX idx_subreddits_name ON subreddits (name); CREATE UNIQUE INDEX idx_subreddits_name ON subreddits (name);

View file

@ -1,12 +0,0 @@
-- +goose Up
-- +goose StatementBegin
INSERT INTO subreddits (name, subtype, schedule) VALUES
('wallpaper', 0, '0 0 * * *'), -- every day at midnight
('wallpapers', 0, '0 0 * * *'); -- every day at midnight
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DELETE FROM subreddits WHERE name IN ('wallpaper', 'wallpapers');
-- +goose StatementEnd

View file

@ -14,18 +14,13 @@ CREATE TABLE devices(
max_y INTEGER NOT NULL DEFAULT 0, max_y INTEGER NOT NULL DEFAULT 0,
nsfw INTEGER NOT NULL DEFAULT 0, nsfw INTEGER NOT NULL DEFAULT 0,
windows_wallpaper_mode INTEGER NOT NULL DEFAULT 0, windows_wallpaper_mode INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at BIGINT DEFAULT 0 NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL updated_at BIGINT DEFAULT 0 NOT NULL
); );
CREATE UNIQUE INDEX idx_devices_name ON devices(slug); CREATE UNIQUE INDEX idx_devices_name ON devices(slug);
CREATE INDEX idx_devices_enable ON devices(enable); CREATE INDEX idx_devices_enable ON devices(enable);
CREATE TRIGGER update_devices_timestamp AFTER UPDATE ON devices FOR EACH ROW
BEGIN
UPDATE devices SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id;
END;
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View file

@ -7,7 +7,7 @@ CREATE TABLE images(
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
post_id VARCHAR(50) NOT NULL, post_id VARCHAR(50) NOT NULL,
post_url VARCHAR(255) NOT NULL, post_url VARCHAR(255) NOT NULL,
post_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, post_created BIGINT NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_name VARCHAR(255) NOT NULL, post_name VARCHAR(255) NOT NULL,
poster VARCHAR(50) NOT NULL, poster VARCHAR(50) NOT NULL,
poster_url VARCHAR(255) NOT NULL, poster_url VARCHAR(255) NOT NULL,
@ -16,8 +16,8 @@ CREATE TABLE images(
image_original_url VARCHAR(255) NOT NULL, image_original_url VARCHAR(255) NOT NULL,
thumbnail_original_url VARCHAR(255) NOT NULL DEFAULT '', thumbnail_original_url VARCHAR(255) NOT NULL DEFAULT '',
nsfw INTEGER NOT NULL DEFAULT 0, nsfw INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at BIGINT DEFAULT 0 NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at BIGINT DEFAULT 0 NOT NULL,
CONSTRAINT fk_subreddit_id CONSTRAINT fk_subreddit_id
FOREIGN KEY (subreddit_id) FOREIGN KEY (subreddit_id)
REFERENCES subreddits(id) REFERENCES subreddits(id)
@ -28,18 +28,10 @@ CREATE TABLE images(
ON DELETE CASCADE ON DELETE CASCADE
); );
CREATE TRIGGER update_images_timestamp AFTER UPDATE ON images FOR EACH ROW CREATE INDEX idx_subreddit_id_images ON images(subreddit_id);
BEGIN CREATE INDEX idx_nsfw_images ON images(nsfw);
UPDATE images SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id; CREATE INDEX idx_images_created_at ON images(created_at DESC);
END; CREATE INDEX idx_unique_device_images_id ON images(device_id, post_id DESC);
CREATE TRIGGER update_subreddits_timestamp_on_insert AFTER INSERT ON images FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = CURRENT_TIMESTAMP WHERE id = new.subreddit_id;
END;
CREATE INDEX idx_subreddit_id ON images(subreddit_id);
CREATE INDEX idx_nsfw ON images(nsfw);
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down

View file

@ -4,13 +4,13 @@ Content-Type: application/json
Content-Length: 211 Content-Length: 211
{ {
"name": "S20FE", "name": "Laptop",
"slug": "s20fe", "slug": "laptop",
"resolution_x": 1080, "resolution_x": 2560,
"resolution_y": 2400, "resolution_y": 1440,
"nsfw": 1, "nsfw": 1,
"aspect_ratio_tolerance": 0.2, "aspect_ratio_tolerance": 0.2,
"enable": 1, "enable": 1,
"min_x": 1080, "min_x": 2560,
"min_y": 2400 "min_y": 1440
} }

View file

@ -2,7 +2,7 @@ POST http://localhost:8080/api/v1/subreddits HTTP/1.1
Host: localhost:8080 Host: localhost:8080
{ {
"name": "AnimeLandscapes", "name": "wallpapers",
"enable_schedule": 1, "enable_schedule": 1,
"schedule": "@daily", "schedule": "@daily",
"countback": 300 "countback": 300

View file

@ -3,5 +3,5 @@ Host: localhost:8080
Content-Type: application/json Content-Type: application/json
{ {
"subreddit": "AnimeLandscapes" "subreddit": "wallpaper"
} }

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"regexp" "regexp"
"github.com/aarondl/opt/omit"
"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"
@ -19,7 +18,7 @@ func (routes *Routes) APIDeviceCreate(rw http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "*Routes.APIDeviceCreate") ctx, span := tracer.Start(r.Context(), "*Routes.APIDeviceCreate")
defer func() { telemetry.EndWithStatus(span, err) }() defer func() { telemetry.EndWithStatus(span, err) }()
var body models.Device var body *models.Device
if err = json.NewDecoder(r.Body).Decode(&body); err != nil { if err = json.NewDecoder(r.Body).Decode(&body); err != nil {
log.New(ctx).Err(err).Error("failed to decode json body") log.New(ctx).Err(err).Error("failed to decode json body")
@ -34,20 +33,7 @@ func (routes *Routes) APIDeviceCreate(rw http.ResponseWriter, r *http.Request) {
return return
} }
device, err := routes.API.DevicesCreate(ctx, &models.DeviceSetter{ device, err := routes.API.DevicesCreate(ctx, body)
Slug: omit.From(body.Slug),
Name: omit.From(body.Name),
ResolutionX: omit.From(body.ResolutionX),
ResolutionY: omit.From(body.ResolutionY),
AspectRatioTolerance: omit.From(body.AspectRatioTolerance),
MinX: omit.From(body.MinX),
MinY: omit.From(body.MinY),
MaxX: omit.From(body.MaxX),
MaxY: omit.From(body.MaxY),
NSFW: omit.From(body.NSFW),
WindowsWallpaperMode: omit.From(body.WindowsWallpaperMode),
Enable: omit.From(body.Enable),
})
if err != nil { if err != nil {
log.New(ctx).Err(err).Error("failed to create device", "body", body) log.New(ctx).Err(err).Error("failed to create device", "body", body)
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)
@ -64,7 +50,7 @@ func (routes *Routes) APIDeviceCreate(rw http.ResponseWriter, r *http.Request) {
var slugRegex = regexp.MustCompile(`^[a-z0-9-]+$`) var slugRegex = regexp.MustCompile(`^[a-z0-9-]+$`)
func validateCreateDevice(params models.Device) error { func validateCreateDevice(params *models.Device) error {
if params.Name == "" { if params.Name == "" {
return errors.New("name is required") return errors.New("name is required")
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/aarondl/opt/omit" "github.com/aarondl/opt/omit"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -59,6 +60,7 @@ func (routes *Routes) APIDeviceUpdate(rw http.ResponseWriter, r *http.Request) {
MaxY: omit.FromPtr(body.MaxY), MaxY: omit.FromPtr(body.MaxY),
NSFW: omit.FromPtr(body.NSFW), NSFW: omit.FromPtr(body.NSFW),
WindowsWallpaperMode: omit.FromPtr(body.WindowsWallpaperMode), WindowsWallpaperMode: omit.FromPtr(body.WindowsWallpaperMode),
UpdatedAt: omit.From(time.Now().Unix()),
}) })
if err != nil { if err != nil {
code, message := errs.HTTPMessage(err) code, message := errs.HTTPMessage(err)

View file

@ -31,7 +31,9 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) {
imageListParams := api.ImageListParams{} imageListParams := api.ImageListParams{}
imageListParams.FillFromQuery(r.URL.Query()) imageListParams.FillFromQuery(r.URL.Query())
if imageListParams.CreatedAt.IsZero() {
imageListParams.CreatedAt = time.Now().Add(-time.Hour * 24) // images in the last 24 hours imageListParams.CreatedAt = time.Now().Add(-time.Hour * 24) // images in the last 24 hours
}
imageListParams.Limit = 0 imageListParams.Limit = 0
imageList, err := routes.API.ImagesListWithDevicesAndSubreddits(ctx, imageListParams) imageList, err := routes.API.ImagesListWithDevicesAndSubreddits(ctx, imageListParams)
@ -49,6 +51,8 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) {
data := homeview.Data{ data := homeview.Data{
SubredditsList: list, SubredditsList: list,
RecentlyAddedImages: homeview.NewRecentlyAddedImages(imageList.Images), RecentlyAddedImages: homeview.NewRecentlyAddedImages(imageList.Images),
Now: time.Now(),
TotalImages: imageList.Total,
} }
if err := homeview.Home(vc, data).Render(ctx, rw); err != nil { if err := homeview.Home(vc, data).Render(ctx, rw); err != nil {

View file

@ -12,7 +12,13 @@ templ Head(vc *views.Context, extras ...templ.Component) {
<script src="/public/htmx-response-targets-1.9.11.min.js"></script> <script src="/public/htmx-response-targets-1.9.11.min.js"></script>
<script src="/public/dayjs-1.11.10.min.js"></script> <script src="/public/dayjs-1.11.10.min.js"></script>
<script src="/public/dayjs-relativeTime-1.11.10.min.js"></script> <script src="/public/dayjs-relativeTime-1.11.10.min.js"></script>
<script>dayjs.extend(window.dayjs_plugin_relativeTime)</script> <script src="/public/dayjs-utc-1.11.10.min.js"></script>
<script src="/public/dayjs-timezone-1.11.10.min.js"></script>
<script>
dayjs.extend(window.dayjs_plugin_relativeTime)
dayjs.extend(window.dayjs_plugin_utc)
dayjs.extend(window.dayjs_plugin_timezone)
</script>
<script src="/public/theme-change-2.0.2.min.js"></script> <script src="/public/theme-change-2.0.2.min.js"></script>
if vc.Config.Bool("http.hotreload") { if vc.Config.Bool("http.hotreload") {
<script src="/public/hot_reload.js"></script> <script src="/public/hot_reload.js"></script>

View file

@ -88,6 +88,15 @@ templ Navbar(c *views.Context) {
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<nav class="pt-4"> <nav class="pt-4">
@navList(c)
</nav>
<div class="flex-grow"></div>
@SelectThemeInput()
</div>
</header>
}
templ navList(c *views.Context) {
<ul class="flex flex-col flex-wrap"> <ul class="flex flex-col flex-wrap">
<a href="/" class={ classForNavItem(c, "/") }> <a href="/" class={ classForNavItem(c, "/") }>
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Home</li> <li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Home</li>
@ -98,15 +107,10 @@ templ Navbar(c *views.Context) {
<a href="/config" class={ classForNavItem(c, "/config") }> <a href="/config" class={ classForNavItem(c, "/config") }>
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Config</li> <li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Config</li>
</a> </a>
<a href="/browse" class={ classForNavItem(c, "/browse") }> <a href="/subreddits" class={ classForNavItem(c, "/subreddits") }>
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Browse</li> <li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Subreddits</li>
</a> </a>
</ul> </ul>
</nav>
<div class="flex-grow"></div>
@SelectThemeInput()
</div>
</header>
} }
templ SelectThemeInput() { templ SelectThemeInput() {

View file

@ -3,7 +3,7 @@ package homeview
import "github.com/tigorlazuardi/redmage/views/components" import "github.com/tigorlazuardi/redmage/views/components"
import "github.com/tigorlazuardi/redmage/views" import "github.com/tigorlazuardi/redmage/views"
import "github.com/tigorlazuardi/redmage/views/utils" import "github.com/tigorlazuardi/redmage/views/utils"
import "time" import "strconv"
templ Home(c *views.Context, data Data) { templ Home(c *views.Context, data Data) {
@components.Doctype() { @components.Doctype() {
@ -17,18 +17,42 @@ templ Home(c *views.Context, data Data) {
// HomeContent returns the main content of the home page. // HomeContent returns the main content of the home page.
// //
// Use this template if request is HX-Boosted. // Use this template if request is HX-Boosted.
templ HomeContent(_ *views.Context, data Data) { templ HomeContent(c *views.Context, data Data) {
<main id="main" class="prose"> <main id="main" class="prose min-w-full">
@components.Container() { @components.Container() {
if data.Error != "" { if data.Error != "" {
@components.ErrorToast(data.Error) @components.ErrorToast(data.Error)
} else { } else {
<section class="mb-4 mx-auto"> <section class="mb-4 mx-auto">
<h1>Recently Added</h1> <div class="flex content-center gap-8">
<h1>
Recently Added -
{ strconv.FormatInt(data.TotalImages, 10) } Images
</h1>
<select
hx-trigger="change"
hx-get="/"
name="created_at"
hx-params="*"
class="select select-ghost"
hx-target="body"
hx-push-url="true"
>
@recentlyRangeOption(c, "-10800", "3 Hours")
@recentlyRangeOption(c, "-21600", "6 Hours")
@recentlyRangeOption(c, "-43200", "12 Hours")
@recentlyRangeOption(c, "-86400", "1 Day")
@recentlyRangeOption(c, "-172800", "2 Days")
@recentlyRangeOption(c, "-259200", "3 Days")
@recentlyRangeOption(c, "-604800", "7 Days")
@recentlyRangeOption(c, "-2592000", "30 Days")
</select>
</div>
for _, recently := range data.RecentlyAddedImages { for _, recently := range data.RecentlyAddedImages {
<h2>{ recently.Device.Name }</h2> <div class="divider"></div>
<h2 class="mt-4">{ recently.Device.Name }</h2>
for _, subreddit := range recently.Subreddits { for _, subreddit := range recently.Subreddits {
<h4>{ subreddit.Subreddit.Name }</h4> <h4>{ subreddit.Subreddit.Name } - { strconv.Itoa(len(subreddit.Images)) } images</h4>
@RecentlyAddedImageList(subreddit.Images, 0) @RecentlyAddedImageList(subreddit.Images, 0)
} }
} }
@ -38,7 +62,7 @@ templ HomeContent(_ *views.Context, data Data) {
for _, subreddit := range data.SubredditsList.Data { for _, subreddit := range data.SubredditsList.Data {
<h3> <h3>
{ subreddit.Name } - { subreddit.Name } -
@utils.RelativeTimeNode(subreddit.Name, utils.NextScheduleTime(subreddit.Schedule).Format(time.RFC3339)) @utils.RelativeTimeNode(subreddit.Name, utils.NextScheduleTime(subreddit.Schedule).Unix())
</h3> </h3>
} }
</section> </section>
@ -46,3 +70,13 @@ templ HomeContent(_ *views.Context, data Data) {
} }
</main> </main>
} }
templ recentlyRangeOption(c *views.Context, value, text string) {
if c.Request.URL.Query().Get("created_at") == "" && value == "-86400" {
<option selected="selected" value={ value }>{ text }</option>
} else if c.Request.URL.Query().Get("created_at") == value {
<option selected="selected" value={ value }>{ text }</option>
} else {
<option value={ value }>{ text }</option>
}
}

View file

@ -1,8 +1,10 @@
package homeview package homeview
import ( import (
"fmt"
"slices" "slices"
"strings" "strings"
"time"
"github.com/tigorlazuardi/redmage/api" "github.com/tigorlazuardi/redmage/api"
"github.com/tigorlazuardi/redmage/models" "github.com/tigorlazuardi/redmage/models"
@ -11,16 +13,13 @@ import (
type Data struct { type Data struct {
SubredditsList api.ListSubredditsResult SubredditsList api.ListSubredditsResult
RecentlyAddedImages RecentlyAddedImages RecentlyAddedImages RecentlyAddedImages
TotalImages int64
Error string Error string
Now time.Time
} }
type RecentlyAddedImages = []RecentlyAddedImage type RecentlyAddedImages = []RecentlyAddedImage
type subredditMapValue struct {
subreddit *models.Subreddit
images []*models.Image
}
type RecentlyAddedImage struct { type RecentlyAddedImage struct {
Device *models.Device Device *models.Device
Subreddits []Subreddit Subreddits []Subreddit
@ -33,6 +32,9 @@ type Subreddit struct {
func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages { func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages {
r := make(RecentlyAddedImages, 0, len(images)) r := make(RecentlyAddedImages, 0, len(images))
var count int
for _, image := range images { for _, image := range images {
if image.R.Device == nil || image.R.Subreddit == nil { if image.R.Device == nil || image.R.Subreddit == nil {
continue continue
@ -46,6 +48,7 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages {
if subreddit.Subreddit.ID == image.R.Subreddit.ID { if subreddit.Subreddit.ID == image.R.Subreddit.ID {
subredditFound = true subredditFound = true
r[i].Subreddits[j].Images = append(r[i].Subreddits[j].Images, image) r[i].Subreddits[j].Images = append(r[i].Subreddits[j].Images, image)
count++
} }
} }
if !subredditFound { if !subredditFound {
@ -53,10 +56,12 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages {
Subreddit: image.R.Subreddit, Subreddit: image.R.Subreddit,
Images: models.ImageSlice{image}, Images: models.ImageSlice{image},
}) })
count++
} }
} }
} }
if !deviceFound { if !deviceFound {
count++
r = append(r, RecentlyAddedImage{ r = append(r, RecentlyAddedImage{
Device: image.R.Device, Device: image.R.Device,
Subreddits: []Subreddit{ Subreddits: []Subreddit{
@ -83,5 +88,7 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages {
return strings.Compare(leftName, rightName) return strings.Compare(leftName, rightName)
}) })
fmt.Println("image count", count)
return r return r
} }

View file

@ -23,6 +23,7 @@ templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
<figure> <figure>
<a <a
href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) } href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) }
target="_blank"
> >
<img <img
class="object-contain w-[256px] h-[256px]" class="object-contain w-[256px] h-[256px]"
@ -57,7 +58,7 @@ templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
} }
templ RecentlyAddedImageList(images models.ImageSlice, opts ImageCardOption) { templ RecentlyAddedImageList(images models.ImageSlice, opts ImageCardOption) {
<div class="w-[80vw] overflow-x-scroll flex gap-4 p-8 shadow-inner bg-base-300"> <div class="overflow-x-auto flex gap-4 p-6 shadow-inner bg-base-300 rounded-2xl w-[85vw] md:w-full">
for _, data := range images { for _, data := range images {
@RecentlyAddedImageCard(data, opts) @RecentlyAddedImageCard(data, opts)
} }

View file

@ -1,19 +1,21 @@
package utils package utils
import "strings" import "strings"
import "strconv"
// RelativeTimeText updates the text content of the element to be a relative time text. // RelativeTimeText updates the text content of the element to be a relative time text.
// //
// Every second it updates the text content to be the relative time text of the input string. // Every second it updates the text content to be the relative time text of the input string.
script RelativeFromTimeText(id string, time string, inter int) { script RelativeFromTimeText(id string, time int64, inter int) {
const el = document.getElementById(id) const el = document.getElementById(id)
el.parentNode.dataset.tip = dayjs.unix(time).tz(dayjs.tz.guess()).format('ddd, D MMM YYYY HH:mm:ss Z')
const timeText = dayjs(time).fromNow() const timeText = dayjs.unix(time).fromNow()
el.textContent = timeText el.textContent = timeText
const interval = setInterval(() => { const interval = setInterval(() => {
const timeText = dayjs(time).fromNow() const timeText = dayjs.unix(time).fromNow()
el.textContent = timeText el.textContent = timeText
}, inter) }, inter)
@ -31,43 +33,12 @@ script RelativeFromTimeText(id string, time string, inter int) {
obs.observe(el.parentNode, { childList: true }) obs.observe(el.parentNode, { childList: true })
} }
script RelativeTimeToNowText(id, time string, inter int) { templ RelativeTimeNode(id string, time int64, class ...string) {
const el = document.getElementById(id) <div class="tooltip z-10" data-tip={ strconv.FormatInt(time, 10) }>
const timeText = dayjs(time).toNow()
el.textContent = timeText
const interval = setInterval(() => {
const timeText = dayjs(time).toNow()
el.textContent = timeText
}, 1000)
const obs = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const removed of mutation.removedNodes) {
if (el === removed) {
clearInterval(interval)
obs.disconnect()
}
}
}
})
obs.observe(el.parentNode, { childList: true })
}
templ RelativeTimeNode(id string, time string, class ...string) {
<span <span
id={ id } id={ id }
class={ strings.Join(class, " ") } class={ strings.Join(class, " ") }
>{ time }</span> >{ strconv.FormatInt(time, 10) }</span>
</div>
@RelativeFromTimeText(id, time, 10000) @RelativeFromTimeText(id, time, 10000)
} }
templ RelativeTimeToNowNode(id, time string, class ...string) {
<span
id={ id }
class={ strings.Join(class, " ") }
>{ time }</span>
@RelativeTimeToNowText(id, time, 10000)
}