diff --git a/Makefile b/Makefile index 73b77dc..895c022 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,16 @@ build-dependencies: 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 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 mkdir -p public echo "Theme change not found, installing it" @@ -70,6 +80,9 @@ migrate-new: migrate-redo: @goose redo + +migrate-down: + @goose down migrate-up: @goose up diff --git a/api/devices_create.go b/api/devices_create.go index a3bd419..bf8929c 100644 --- a/api/devices_create.go +++ b/api/devices_create.go @@ -3,7 +3,9 @@ package api import ( "context" "errors" + "time" + "github.com/aarondl/opt/omit" "github.com/mattn/go-sqlite3" "github.com/tigorlazuardi/redmage/models" "github.com/tigorlazuardi/redmage/pkg/errs" @@ -11,11 +13,27 @@ import ( 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") 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 { var sqliteErr sqlite3.Error if errors.As(err, &sqliteErr) { diff --git a/api/download_subreddit_images.go b/api/download_subreddit_images.go index 6455718..f55d6c4 100644 --- a/api/download_subreddit_images.go +++ b/api/download_subreddit_images.go @@ -186,6 +186,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su } var many []*models.ImageSetter + now := time.Now() for _, device := range devices { var nsfw int32 if post.IsNSFW() { @@ -197,7 +198,7 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su Title: omit.From(post.GetTitle()), PostID: omit.From(post.GetID()), PostURL: omit.From(post.GetPostURL()), - PostCreated: omit.From(post.GetCreated().Format(time.RFC3339)), + PostCreated: omit.From(post.GetCreated().Unix()), PostName: omit.From(post.GetName()), Poster: omit.From(post.GetAuthor()), 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()), ThumbnailOriginalURL: omit.From(post.GetThumbnailURL()), NSFW: omit.From(nsfw), + CreatedAt: omit.From(now.Unix()), + UpdatedAt: omit.From(now.Unix()), }) } diff --git a/api/images_list.go b/api/images_list.go index fe13645..7cee855 100644 --- a/api/images_list.go +++ b/api/images_list.go @@ -48,6 +48,9 @@ func (ilp *ImageListParams) FillFromQuery(query Queryable) { createdAtint, _ := strconv.ParseInt(query.Get("created_at"), 10, 64) if 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() { - 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 diff --git a/api/subreddits_create.go b/api/subreddits_create.go index 92a0e21..538e9d6 100644 --- a/api/subreddits_create.go +++ b/api/subreddits_create.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "time" "github.com/aarondl/opt/omit" "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") defer span.End() + now := time.Now() set := &models.SubredditSetter{ Name: omit.From(params.Name), EnableSchedule: omit.From(params.EnableSchedule), Subtype: omit.From(params.Subtype), Schedule: omit.From(params.Schedule), Countback: omit.From(params.Countback), + CreatedAt: omit.From(now.Unix()), + UpdatedAt: omit.From(now.Unix()), } subreddit, err = models.Subreddits.Insert(ctx, api.db, set) diff --git a/db/migrations/20240406220949_create_subreddits_table.sql b/db/migrations/20240406220949_create_subreddits_table.sql index 5673afa..e751d59 100644 --- a/db/migrations/20240406220949_create_subreddits_table.sql +++ b/db/migrations/20240406220949_create_subreddits_table.sql @@ -5,10 +5,10 @@ CREATE TABLE subreddits ( name VARCHAR(30) NOT NULL, enable_schedule INT NOT NULL DEFAULT 1, 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, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + created_at BIGINT DEFAULT 0 NOT NULL, + updated_at BIGINT DEFAULT 0 NOT NULL ); CREATE UNIQUE INDEX idx_subreddits_name ON subreddits (name); diff --git a/db/migrations/20240407001522_insert_default_subreddits.sql b/db/migrations/20240407001522_insert_default_subreddits.sql deleted file mode 100644 index 61d7841..0000000 --- a/db/migrations/20240407001522_insert_default_subreddits.sql +++ /dev/null @@ -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 diff --git a/db/migrations/20240409221254_create_table_device.sql b/db/migrations/20240409221254_create_table_device.sql index b8940f9..bea6789 100644 --- a/db/migrations/20240409221254_create_table_device.sql +++ b/db/migrations/20240409221254_create_table_device.sql @@ -14,18 +14,13 @@ CREATE TABLE devices( max_y INTEGER NOT NULL DEFAULT 0, nsfw INTEGER NOT NULL DEFAULT 0, windows_wallpaper_mode INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + created_at BIGINT DEFAULT 0 NOT NULL, + updated_at BIGINT DEFAULT 0 NOT NULL ); CREATE UNIQUE INDEX idx_devices_name ON devices(slug); 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 Down diff --git a/db/migrations/20240409222145_create_images_table.sql b/db/migrations/20240409222145_create_images_table.sql index 9902579..12a1046 100644 --- a/db/migrations/20240409222145_create_images_table.sql +++ b/db/migrations/20240409222145_create_images_table.sql @@ -7,7 +7,7 @@ CREATE TABLE images( title VARCHAR(255) NOT NULL, post_id VARCHAR(50) 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, poster VARCHAR(50) NOT NULL, poster_url VARCHAR(255) NOT NULL, @@ -16,8 +16,8 @@ CREATE TABLE images( image_original_url VARCHAR(255) NOT NULL, thumbnail_original_url VARCHAR(255) NOT NULL DEFAULT '', nsfw INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at BIGINT DEFAULT 0 NOT NULL, + updated_at BIGINT DEFAULT 0 NOT NULL, CONSTRAINT fk_subreddit_id FOREIGN KEY (subreddit_id) REFERENCES subreddits(id) @@ -28,18 +28,10 @@ CREATE TABLE images( ON DELETE CASCADE ); -CREATE TRIGGER update_images_timestamp AFTER UPDATE ON images FOR EACH ROW -BEGIN - UPDATE images SET updated_at = CURRENT_TIMESTAMP WHERE id = old.id; -END; - -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); +CREATE INDEX idx_subreddit_id_images ON images(subreddit_id); +CREATE INDEX idx_nsfw_images ON images(nsfw); +CREATE INDEX idx_images_created_at ON images(created_at DESC); +CREATE INDEX idx_unique_device_images_id ON images(device_id, post_id DESC); -- +goose StatementEnd -- +goose Down diff --git a/rest/devices/create.http b/rest/devices/create.http index f674fc0..4c28999 100644 --- a/rest/devices/create.http +++ b/rest/devices/create.http @@ -4,13 +4,13 @@ Content-Type: application/json Content-Length: 211 { - "name": "S20FE", - "slug": "s20fe", - "resolution_x": 1080, - "resolution_y": 2400, + "name": "Laptop", + "slug": "laptop", + "resolution_x": 2560, + "resolution_y": 1440, "nsfw": 1, "aspect_ratio_tolerance": 0.2, "enable": 1, - "min_x": 1080, - "min_y": 2400 + "min_x": 2560, + "min_y": 1440 } diff --git a/rest/subreddits/create.http b/rest/subreddits/create.http index 4ff3d4a..1297c29 100644 --- a/rest/subreddits/create.http +++ b/rest/subreddits/create.http @@ -2,7 +2,7 @@ POST http://localhost:8080/api/v1/subreddits HTTP/1.1 Host: localhost:8080 { - "name": "AnimeLandscapes", + "name": "wallpapers", "enable_schedule": 1, "schedule": "@daily", "countback": 300 diff --git a/rest/subreddits/start.http b/rest/subreddits/start.http index 36d7049..b0b80f2 100644 --- a/rest/subreddits/start.http +++ b/rest/subreddits/start.http @@ -3,5 +3,5 @@ Host: localhost:8080 Content-Type: application/json { - "subreddit": "AnimeLandscapes" + "subreddit": "wallpaper" } diff --git a/server/routes/device_create.go b/server/routes/device_create.go index 705b4f0..03c2bf7 100644 --- a/server/routes/device_create.go +++ b/server/routes/device_create.go @@ -7,7 +7,6 @@ import ( "net/http" "regexp" - "github.com/aarondl/opt/omit" "github.com/tigorlazuardi/redmage/models" "github.com/tigorlazuardi/redmage/pkg/errs" "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") defer func() { telemetry.EndWithStatus(span, err) }() - var body models.Device + var body *models.Device if err = json.NewDecoder(r.Body).Decode(&body); err != nil { 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 } - device, err := routes.API.DevicesCreate(ctx, &models.DeviceSetter{ - 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), - }) + device, err := routes.API.DevicesCreate(ctx, body) if err != nil { log.New(ctx).Err(err).Error("failed to create device", "body", body) 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-]+$`) -func validateCreateDevice(params models.Device) error { +func validateCreateDevice(params *models.Device) error { if params.Name == "" { return errors.New("name is required") } diff --git a/server/routes/device_update.go b/server/routes/device_update.go index dd52f72..4980243 100644 --- a/server/routes/device_update.go +++ b/server/routes/device_update.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/aarondl/opt/omit" "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), NSFW: omit.FromPtr(body.NSFW), WindowsWallpaperMode: omit.FromPtr(body.WindowsWallpaperMode), + UpdatedAt: omit.From(time.Now().Unix()), }) if err != nil { code, message := errs.HTTPMessage(err) diff --git a/server/routes/page_home.go b/server/routes/page_home.go index 8aadf8f..5bc1d27 100644 --- a/server/routes/page_home.go +++ b/server/routes/page_home.go @@ -31,7 +31,9 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) { imageListParams := api.ImageListParams{} 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 imageList, err := routes.API.ImagesListWithDevicesAndSubreddits(ctx, imageListParams) @@ -49,6 +51,8 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) { data := homeview.Data{ SubredditsList: list, RecentlyAddedImages: homeview.NewRecentlyAddedImages(imageList.Images), + Now: time.Now(), + TotalImages: imageList.Total, } if err := homeview.Home(vc, data).Render(ctx, rw); err != nil { diff --git a/views/components/head.templ b/views/components/head.templ index 5a0b6fb..5998b46 100644 --- a/views/components/head.templ +++ b/views/components/head.templ @@ -12,7 +12,13 @@ templ Head(vc *views.Context, extras ...templ.Component) { - + + + if vc.Config.Bool("http.hotreload") { diff --git a/views/components/navigation.templ b/views/components/navigation.templ index c5d1af0..a9b0991 100644 --- a/views/components/navigation.templ +++ b/views/components/navigation.templ @@ -88,20 +88,7 @@ templ Navbar(c *views.Context) {
@SelectThemeInput() @@ -109,6 +96,23 @@ templ Navbar(c *views.Context) { } +templ navList(c *views.Context) { + +} + templ SelectThemeInput() { + @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") + + for _, recently := range data.RecentlyAddedImages { -

{ recently.Device.Name }

+
+

{ recently.Device.Name }

for _, subreddit := range recently.Subreddits { -

{ subreddit.Subreddit.Name }

+

{ subreddit.Subreddit.Name } - { strconv.Itoa(len(subreddit.Images)) } images

@RecentlyAddedImageList(subreddit.Images, 0) } } @@ -38,7 +62,7 @@ templ HomeContent(_ *views.Context, data Data) { for _, subreddit := range data.SubredditsList.Data {

{ subreddit.Name } - - @utils.RelativeTimeNode(subreddit.Name, utils.NextScheduleTime(subreddit.Schedule).Format(time.RFC3339)) + @utils.RelativeTimeNode(subreddit.Name, utils.NextScheduleTime(subreddit.Schedule).Unix())

} @@ -46,3 +70,13 @@ templ HomeContent(_ *views.Context, data Data) { } } + +templ recentlyRangeOption(c *views.Context, value, text string) { + if c.Request.URL.Query().Get("created_at") == "" && value == "-86400" { + + } else if c.Request.URL.Query().Get("created_at") == value { + + } else { + + } +} diff --git a/views/homeview/homeview_data.go b/views/homeview/homeview_data.go index 313f173..a7fb295 100644 --- a/views/homeview/homeview_data.go +++ b/views/homeview/homeview_data.go @@ -1,8 +1,10 @@ package homeview import ( + "fmt" "slices" "strings" + "time" "github.com/tigorlazuardi/redmage/api" "github.com/tigorlazuardi/redmage/models" @@ -11,16 +13,13 @@ import ( type Data struct { SubredditsList api.ListSubredditsResult RecentlyAddedImages RecentlyAddedImages + TotalImages int64 Error string + Now time.Time } type RecentlyAddedImages = []RecentlyAddedImage -type subredditMapValue struct { - subreddit *models.Subreddit - images []*models.Image -} - type RecentlyAddedImage struct { Device *models.Device Subreddits []Subreddit @@ -33,6 +32,9 @@ type Subreddit struct { func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages { r := make(RecentlyAddedImages, 0, len(images)) + + var count int + for _, image := range images { if image.R.Device == nil || image.R.Subreddit == nil { continue @@ -46,6 +48,7 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages { if subreddit.Subreddit.ID == image.R.Subreddit.ID { subredditFound = true r[i].Subreddits[j].Images = append(r[i].Subreddits[j].Images, image) + count++ } } if !subredditFound { @@ -53,10 +56,12 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages { Subreddit: image.R.Subreddit, Images: models.ImageSlice{image}, }) + count++ } } } if !deviceFound { + count++ r = append(r, RecentlyAddedImage{ Device: image.R.Device, Subreddits: []Subreddit{ @@ -83,5 +88,7 @@ func NewRecentlyAddedImages(images models.ImageSlice) RecentlyAddedImages { return strings.Compare(leftName, rightName) }) + fmt.Println("image count", count) + return r } diff --git a/views/homeview/recently_added_image.templ b/views/homeview/recently_added_image.templ index 40c3ef6..7ce64af 100644 --- a/views/homeview/recently_added_image.templ +++ b/views/homeview/recently_added_image.templ @@ -23,6 +23,7 @@ templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
+
for _, data := range images { @RecentlyAddedImageCard(data, opts) } diff --git a/views/utils/relative_schedule_time.templ b/views/utils/relative_schedule_time.templ index f467ab2..416c393 100644 --- a/views/utils/relative_schedule_time.templ +++ b/views/utils/relative_schedule_time.templ @@ -1,19 +1,21 @@ package utils import "strings" +import "strconv" // 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. -script RelativeFromTimeText(id string, time string, inter int) { +script RelativeFromTimeText(id string, time int64, inter int) { const el = document.getElementById(id) - - const timeText = dayjs(time).fromNow() + el.parentNode.dataset.tip = dayjs.unix(time).tz(dayjs.tz.guess()).format('ddd, D MMM YYYY HH:mm:ss Z') + + const timeText = dayjs.unix(time).fromNow() el.textContent = timeText const interval = setInterval(() => { - const timeText = dayjs(time).fromNow() + const timeText = dayjs.unix(time).fromNow() el.textContent = timeText }, inter) @@ -31,43 +33,12 @@ script RelativeFromTimeText(id string, time string, inter int) { obs.observe(el.parentNode, { childList: true }) } -script RelativeTimeToNowText(id, time string, inter int) { - const el = document.getElementById(id) - - 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) { - { time } +templ RelativeTimeNode(id string, time int64, class ...string) { +
+ { strconv.FormatInt(time, 10) } +
@RelativeFromTimeText(id, time, 10000) } - -templ RelativeTimeToNowNode(id, time string, class ...string) { - { time } - @RelativeTimeToNowText(id, time, 10000) -}