Kaynağa Gözat

Add detailed-list style for RSS

Svilen Markov 1 yıl önce
ebeveyn
işleme
d18f645c18

+ 58 - 6
internal/assets/static/main.css

@@ -37,6 +37,7 @@
 
     --ths: var(--bgh), calc(var(--bgs) * var(--tsm));
     --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-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%));
 
@@ -79,14 +80,16 @@
     white-space: nowrap;
 }
 
-.text-truncate-3-lines {
+.text-truncate-2-lines, .text-truncate-3-lines {
     overflow: hidden;
     text-overflow: ellipsis;
-    -webkit-line-clamp: 3;
     display: -webkit-box;
     -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.text-truncate::before,
 .bookmarks-link:not(.bookmarks-link-no-arrow)::after {
@@ -114,6 +117,7 @@
 .list-gap-14 { --list-half-gap: 0.7rem; }
 .list-gap-20 { --list-half-gap: 1rem; }
 .list-gap-24 { --list-half-gap: 1.2rem; }
+.list-gap-34 { --list-half-gap: 1.7rem; }
 
 .list > *:not(:first-child) {
     margin-top: calc(var(--list-half-gap) * 2);
@@ -190,6 +194,20 @@
     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 {
     background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
     color: var(--color-text-highlight);
@@ -883,7 +901,18 @@ body {
     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;
     filter: none;
 }
@@ -931,6 +960,20 @@ body {
     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 {
     width: 5rem;
     border-radius: var(--border-radius);
@@ -1171,11 +1214,11 @@ body {
 
     .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
 
-    .forum-post-list-item {
-        flex-flow: row-reverse;
+    .row-reverse-on-mobile {
+        flex-direction: row-reverse;
     }
 
-    .hide-on-mobile {
+    .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) {
         display: none
     }
 
@@ -1187,6 +1230,14 @@ body {
         color: var(--color-text-highlight);
         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); }
@@ -1250,3 +1301,4 @@ body {
 .margin-bottom-10   { margin-bottom: 1rem; }
 .margin-bottom-15   { margin-bottom: 1.5rem; }
 .margin-bottom-auto { margin-bottom: auto; }
+.scale-half         { transform: scale(0.5); }

+ 1 - 0
internal/assets/templates.go

@@ -27,6 +27,7 @@ var (
 	VideosGridTemplate            = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
 	StocksTemplate                = compileTemplate("stocks.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")
 	RSSHorizontalCards2Template   = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html")
 	MonitorTemplate               = compileTemplate("monitor.html", "widget-base.html")

+ 1 - 1
internal/assets/templates/forum-posts.html

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

+ 38 - 0
internal/assets/templates/rss-detailed-list.html

@@ -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 }}

+ 1 - 1
internal/assets/templates/rss-horizontal-cards-2.html

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

+ 1 - 1
internal/assets/templates/rss-horizontal-cards.html

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

+ 1 - 1
internal/assets/templates/twitch-channels.html

@@ -4,7 +4,7 @@
 <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
     {{ range .Channels }}
     <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">
                 {{ if .Exists }}
                 <img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">

+ 1 - 1
internal/assets/templates/twitch-games-list.html

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

+ 1 - 1
internal/assets/templates/videos-grid.html

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

+ 1 - 1
internal/assets/templates/videos.html

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

+ 57 - 2
internal/feed/rss.go

@@ -3,8 +3,11 @@ package feed
 import (
 	"context"
 	"fmt"
+	"html"
 	"log/slog"
+	"regexp"
 	"sort"
+	"strings"
 	"time"
 
 	"github.com/mmcdole/gofeed"
@@ -16,12 +19,34 @@ type RSSFeedItem struct {
 	Title       string
 	Link        string
 	ImageURL    string
+	Categories  []string
+	Description string
 	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 {
-	Url   string `yaml:"url"`
-	Title string `yaml:"title"`
+	Url             string `yaml:"url"`
+	Title           string `yaml:"title"`
+	HideCategories  bool   `yaml:"hide-categories"`
+	HideDescription bool   `yaml:"hide-description"`
 }
 
 type RSSFeedItems []RSSFeedItem
@@ -57,6 +82,36 @@ func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
 			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 != "" {
 			rssItem.ChannelName = request.Title
 		} else {

+ 10 - 0
internal/feed/utils.go

@@ -77,3 +77,13 @@ func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
 
 	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
+}

+ 11 - 0
internal/widget/rss.go

@@ -39,6 +39,13 @@ func (widget *RSS) Initialize() error {
 		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
 }
 
@@ -65,5 +72,9 @@ func (widget *RSS) Render() template.HTML {
 		return widget.render(widget, assets.RSSHorizontalCards2Template)
 	}
 
+	if widget.Style == "detailed-list" {
+		return widget.render(widget, assets.RSSDetailedListTemplate)
+	}
+
 	return widget.render(widget, assets.RSSListTemplate)
 }