Add detailed-list style for RSS

This commit is contained in:
Svilen Markov 2024-05-21 17:28:30 +01:00
parent 6663360caa
commit d18f645c18
13 changed files with 182 additions and 15 deletions

View File

@ -37,6 +37,7 @@
--ths: var(--bgh), calc(var(--bgs) * var(--tsm)); --ths: var(--bgh), calc(var(--bgs) * var(--tsm));
--color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%)); --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
--color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%));
--color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%)); --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%));
--color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%)); --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%));
@ -79,14 +80,16 @@
white-space: nowrap; white-space: nowrap;
} }
.text-truncate-3-lines { .text-truncate-2-lines, .text-truncate-3-lines {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
-webkit-line-clamp: 3;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.text-truncate-3-lines { -webkit-line-clamp: 3; }
.text-truncate-2-lines { -webkit-line-clamp: 2; }
.visited-indicator:not(.text-truncate)::after, .visited-indicator:not(.text-truncate)::after,
.visited-indicator.text-truncate::before, .visited-indicator.text-truncate::before,
.bookmarks-link:not(.bookmarks-link-no-arrow)::after { .bookmarks-link:not(.bookmarks-link-no-arrow)::after {
@ -114,6 +117,7 @@
.list-gap-14 { --list-half-gap: 0.7rem; } .list-gap-14 { --list-half-gap: 0.7rem; }
.list-gap-20 { --list-half-gap: 1rem; } .list-gap-20 { --list-half-gap: 1rem; }
.list-gap-24 { --list-half-gap: 1.2rem; } .list-gap-24 { --list-half-gap: 1.2rem; }
.list-gap-34 { --list-half-gap: 1.7rem; }
.list > *:not(:first-child) { .list > *:not(:first-child) {
margin-top: calc(var(--list-half-gap) * 2); margin-top: calc(var(--list-half-gap) * 2);
@ -190,6 +194,20 @@
background-color: var(--color-background); background-color: var(--color-background);
} }
.attachments {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-left: -0.5rem;
}
.attachments > * {
border-radius: var(--border-radius);
padding: 0.1rem 0.5rem;
font-size: var(--font-size-h6);
background-color: var(--color-separator);
}
::selection { ::selection {
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
color: var(--color-text-highlight); color: var(--color-text-highlight);
@ -883,7 +901,18 @@ body {
transition: filter 0.2s, opacity .2s; transition: filter 0.2s, opacity .2s;
} }
.thumbnail-container:hover .thumbnail { .thumbnail-container {
flex-shrink: 0;
border: 1px solid var(--color-separator);
border-radius: var(--border-radius);
}
.thumbnail-container > * {
border-radius: var(--border-radius);
object-fit: cover;
}
.thumbnail-parent:hover .thumbnail {
opacity: 1; opacity: 1;
filter: none; filter: none;
} }
@ -931,6 +960,20 @@ body {
z-index: 3; z-index: 3;
} }
.rss-detailed-description {
max-width: 55rem;
color: var(--color-text-base-muted);
}
.rss-detailed-thumbnail {
margin-top: 0.3rem;
}
.rss-detailed-thumbnail > * {
aspect-ratio: 3 / 2;
height: 8.7rem;
}
.twitch-category-thumbnail { .twitch-category-thumbnail {
width: 5rem; width: 5rem;
border-radius: var(--border-radius); border-radius: var(--border-radius);
@ -1171,11 +1214,11 @@ body {
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
.forum-post-list-item { .row-reverse-on-mobile {
flex-flow: row-reverse; flex-direction: row-reverse;
} }
.hide-on-mobile { .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) {
display: none display: none
} }
@ -1187,6 +1230,14 @@ body {
color: var(--color-text-highlight); color: var(--color-text-highlight);
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
} }
.rss-detailed-thumbnail > * {
height: 6rem;
}
.rss-detailed-description {
-webkit-line-clamp: 3;
}
} }
.size-h1 { font-size: var(--font-size-h1); } .size-h1 { font-size: var(--font-size-h1); }
@ -1250,3 +1301,4 @@ body {
.margin-bottom-10 { margin-bottom: 1rem; } .margin-bottom-10 { margin-bottom: 1rem; }
.margin-bottom-15 { margin-bottom: 1.5rem; } .margin-bottom-15 { margin-bottom: 1.5rem; }
.margin-bottom-auto { margin-bottom: auto; } .margin-bottom-auto { margin-bottom: auto; }
.scale-half { transform: scale(0.5); }

View File

@ -27,6 +27,7 @@ var (
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html") VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
StocksTemplate = compileTemplate("stocks.html", "widget-base.html") StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html") RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
RSSDetailedListTemplate = compileTemplate("rss-detailed-list.html", "widget-base.html")
RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html") RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html")
RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html") RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html")
MonitorTemplate = compileTemplate("monitor.html", "widget-base.html") MonitorTemplate = compileTemplate("monitor.html", "widget-base.html")

View File

@ -4,7 +4,7 @@
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Posts }} {{ range .Posts }}
<li> <li>
<div class="forum-post-list-item thumbnail-container"> <div class="flex gap-10 row-reverse-on-mobile thumbnail-parent">
{{ if $.ShowThumbnails }} {{ if $.ShowThumbnails }}
{{ if ne .ThumbnailUrl "" }} {{ if ne .ThumbnailUrl "" }}
<img class="forum-post-list-thumbnail thumbnail" src="{{ .ThumbnailUrl }}" alt="" loading="lazy"> <img class="forum-post-list-thumbnail thumbnail" src="{{ .ThumbnailUrl }}" alt="" loading="lazy">

View File

@ -0,0 +1,38 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-24 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Items }}
<li class="flex gap-15 items-start row-reverse-on-mobile thumbnail-parent">
<div class="thumbnail-container rss-detailed-thumbnail">
{{ if ne "" .ImageURL }}
<img class="thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
{{ else }}
<svg class="scale-half hide-on-mobile" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
{{ end }}
</div>
<div class="grow min-width-0">
<a class="size-h3 color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap">
<li {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
<li class="min-width-0">
<a class="block text-truncate" href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a>
</li>
</ul>
{{ if ne "" .Description }}
<p class="rss-detailed-description text-truncate-2-lines margin-top-10">{{ .Description }}</p>
{{ end }}
{{ if gt (len .Categories) 0 }}
<ul class="attachments margin-top-10">
{{ range .Categories }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ end }}
</div>
</li>
{{ end }}
</ul>
{{ end }}

View File

@ -6,7 +6,7 @@
<div class="carousel-container"> <div class="carousel-container">
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}> <div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}>
{{ range .Items }} {{ range .Items }}
<div class="card rss-card-2 widget-content-frame thumbnail-container"> <div class="card rss-card-2 widget-content-frame thumbnail-parent">
{{ if ne "" .ImageURL }} {{ if ne "" .ImageURL }}
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt=""> <img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
{{ else }} {{ else }}

View File

@ -6,7 +6,7 @@
<div class="carousel-container"> <div class="carousel-container">
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .ThumbnailHeight }} style="--rss-thumbnail-height: {{ .ThumbnailHeight }}rem;"{{ end }}> <div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .ThumbnailHeight }} style="--rss-thumbnail-height: {{ .ThumbnailHeight }}rem;"{{ end }}>
{{ range .Items }} {{ range .Items }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-parent">
{{ if ne "" .ImageURL }} {{ if ne "" .ImageURL }}
<img class="rss-card-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt=""> <img class="rss-card-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
{{ else }} {{ else }}

View File

@ -4,7 +4,7 @@
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Channels }} {{ range .Channels }}
<li> <li>
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container"> <div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-parent">
<div class="twitch-channel-avatar-container"> <div class="twitch-channel-avatar-container">
{{ if .Exists }} {{ if .Exists }}
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy"> <img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">

View File

@ -3,7 +3,7 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range .Categories }} {{ range .Categories }}
<li class="twitch-category thumbnail-container"> <li class="twitch-category thumbnail-parent">
<div class="flex gap-10 items-center"> <div class="flex gap-10 items-center">
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ .AvatarUrl }}" alt=""> <img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ .AvatarUrl }}" alt="">
<div class="min-width-0"> <div class="min-width-0">

View File

@ -5,7 +5,7 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}"> <div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
{{ range .Videos }} {{ range .Videos }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-parent">
{{ template "video-card-contents" . }} {{ template "video-card-contents" . }}
</div> </div>
{{ end }} {{ end }}

View File

@ -6,7 +6,7 @@
<div class="carousel-container"> <div class="carousel-container">
<div class="cards-horizontal carousel-items-container"> <div class="cards-horizontal carousel-items-container">
{{ range .Videos }} {{ range .Videos }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-parent">
{{ template "video-card-contents" . }} {{ template "video-card-contents" . }}
</div> </div>
{{ end }} {{ end }}

View File

@ -3,8 +3,11 @@ package feed
import ( import (
"context" "context"
"fmt" "fmt"
"html"
"log/slog" "log/slog"
"regexp"
"sort" "sort"
"strings"
"time" "time"
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
@ -16,12 +19,34 @@ type RSSFeedItem struct {
Title string Title string
Link string Link string
ImageURL string ImageURL string
Categories []string
Description string
PublishedAt time.Time PublishedAt time.Time
} }
// doesn't cover all cases but works the vast majority of the time
var htmlTagsWithAttributesPattern = regexp.MustCompile(`<\/?[a-zA-Z0-9-]+ *(?:[a-zA-Z-]+=(?:"|').*?(?:"|') ?)* *\/?>`)
var sequentialWhitespacePattern = regexp.MustCompile(`\s+`)
func sanitizeFeedDescription(description string) string {
if description == "" {
return ""
}
description = strings.ReplaceAll(description, "\n", " ")
description = htmlTagsWithAttributesPattern.ReplaceAllString(description, "")
description = sequentialWhitespacePattern.ReplaceAllString(description, " ")
description = strings.TrimSpace(description)
description = html.UnescapeString(description)
return description
}
type RSSFeedRequest struct { type RSSFeedRequest struct {
Url string `yaml:"url"` Url string `yaml:"url"`
Title string `yaml:"title"` Title string `yaml:"title"`
HideCategories bool `yaml:"hide-categories"`
HideDescription bool `yaml:"hide-description"`
} }
type RSSFeedItems []RSSFeedItem type RSSFeedItems []RSSFeedItem
@ -57,6 +82,36 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
Link: item.Link, Link: item.Link,
} }
if !request.HideDescription && item.Description != "" {
description, _ := limitStringLength(item.Description, 1000)
description = sanitizeFeedDescription(description)
description, limited := limitStringLength(description, 200)
if limited {
description += "…"
}
rssItem.Description = description
}
if !request.HideCategories {
var categories = make([]string, 0, 6)
for _, category := range item.Categories {
if len(categories) == 6 {
break
}
if len(category) == 0 || len(category) > 30 {
continue
}
categories = append(categories, category)
}
rssItem.Categories = categories
}
if request.Title != "" { if request.Title != "" {
rssItem.ChannelName = request.Title rssItem.ChannelName = request.Title
} else { } else {

View File

@ -77,3 +77,13 @@ func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
return values return values
} }
func limitStringLength(s string, max int) (string, bool) {
asRunes := []rune(s)
if len(asRunes) > max {
return string(asRunes[:max]), true
}
return s, false
}

View File

@ -39,6 +39,13 @@ func (widget *RSS) Initialize() error {
widget.CardHeight = 0 widget.CardHeight = 0
} }
if widget.Style != "detailed-list" {
for i := range widget.FeedRequests {
widget.FeedRequests[i].HideCategories = true
widget.FeedRequests[i].HideDescription = true
}
}
return nil return nil
} }
@ -65,5 +72,9 @@ func (widget *RSS) Render() template.HTML {
return widget.render(widget, assets.RSSHorizontalCards2Template) return widget.render(widget, assets.RSSHorizontalCards2Template)
} }
if widget.Style == "detailed-list" {
return widget.render(widget, assets.RSSDetailedListTemplate)
}
return widget.render(widget, assets.RSSListTemplate) return widget.render(widget, assets.RSSListTemplate)
} }