sqlite: date based columns migrated to int64
This commit is contained in:
parent
6c973cdf1f
commit
2b4df20754
13
Makefile
13
Makefile
|
@ -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"
|
||||||
|
@ -70,6 +80,9 @@ migrate-new:
|
||||||
|
|
||||||
migrate-redo:
|
migrate-redo:
|
||||||
@goose redo
|
@goose redo
|
||||||
|
|
||||||
|
migrate-down:
|
||||||
|
@goose down
|
||||||
|
|
||||||
migrate-up:
|
migrate-up:
|
||||||
@goose up
|
@goose up
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -3,5 +3,5 @@ Host: localhost:8080
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"subreddit": "AnimeLandscapes"
|
"subreddit": "wallpaper"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
imageListParams.CreatedAt = time.Now().Add(-time.Hour * 24) // images in the last 24 hours
|
if imageListParams.CreatedAt.IsZero() {
|
||||||
|
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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -88,20 +88,7 @@ templ Navbar(c *views.Context) {
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<nav class="pt-4">
|
<nav class="pt-4">
|
||||||
<ul class="flex flex-col flex-wrap">
|
@navList(c)
|
||||||
<a href="/" class={ classForNavItem(c, "/") }>
|
|
||||||
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Home</li>
|
|
||||||
</a>
|
|
||||||
<a href="/about" class={ classForNavItem(c, "/about") }>
|
|
||||||
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">About</li>
|
|
||||||
</a>
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
<a href="/browse" class={ classForNavItem(c, "/browse") }>
|
|
||||||
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Browse</li>
|
|
||||||
</a>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
@SelectThemeInput()
|
@SelectThemeInput()
|
||||||
|
@ -109,6 +96,23 @@ templ Navbar(c *views.Context) {
|
||||||
</header>
|
</header>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ navList(c *views.Context) {
|
||||||
|
<ul class="flex flex-col flex-wrap">
|
||||||
|
<a href="/" class={ classForNavItem(c, "/") }>
|
||||||
|
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Home</li>
|
||||||
|
</a>
|
||||||
|
<a href="/about" class={ classForNavItem(c, "/about") }>
|
||||||
|
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">About</li>
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
<a href="/subreddits" class={ classForNavItem(c, "/subreddits") }>
|
||||||
|
<li class="hover:bg-accent hover:text-neutral-50 py-2 text-center hover:font-bold">Subreddits</li>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
templ SelectThemeInput() {
|
templ SelectThemeInput() {
|
||||||
<select class="select select-ghost select-bordered w-full" data-choose-theme>
|
<select class="select select-ghost select-bordered w-full" data-choose-theme>
|
||||||
<option value="light">Light (Default)</option>
|
<option value="light">Light (Default)</option>
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }>
|
||||||
|
<span
|
||||||
const timeText = dayjs(time).toNow()
|
id={ id }
|
||||||
el.textContent = timeText
|
class={ strings.Join(class, " ") }
|
||||||
|
>{ strconv.FormatInt(time, 10) }</span>
|
||||||
const interval = setInterval(() => {
|
</div>
|
||||||
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
|
|
||||||
id={ id }
|
|
||||||
class={ strings.Join(class, " ") }
|
|
||||||
>{ time }</span>
|
|
||||||
@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)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue