Compare commits

...

31 Commits

Author SHA1 Message Date
8814467db9 chore: cleaned up logging 2021-07-24 12:44:38 +01:00
caa0b01937 chore: removed unused template functions 2021-07-24 12:42:43 +01:00
ea8c2e4bbb Set line height for item headings 2021-05-15 15:27:11 +01:00
7613116bf3 Only automatically update when page not being viewed 2021-05-15 11:31:55 +01:00
a21dd6b74f Added loading indicator 2021-05-15 11:24:50 +01:00
f5b386f70d Only scroll into view if not already visible 2021-05-15 11:10:46 +01:00
da1319ca68 Switched font 2021-05-15 11:03:19 +01:00
9306e6cbe6 Ensure selectedItem has a value 2021-03-18 14:25:44 +00:00
9f4e07d5a5 Toggle iframe on and off 2021-03-17 09:07:46 +00:00
42014bdaa1 Find feed url from webpage 2021-03-17 08:04:38 +00:00
47c72d0029 Replace items with unread on background sync 2021-03-16 21:45:27 +00:00
761b4f05fd Merge branch 'master' of https://git.cluster.fun/AverageMarcus/gopherss 2021-03-03 19:55:17 +00:00
9c07dd226e Fixed href rewrite 2021-03-03 19:54:54 +00:00
b2e70bb0f7 Added missing link tag 2021-02-22 10:09:20 +00:00
00eeef7ec6 Added favicon 2021-02-22 09:46:07 +00:00
a96a43f9ec Fix h1 margin 2021-02-21 11:04:01 +00:00
1fc26bfdfe Sort feeds 2021-02-21 10:34:44 +00:00
22db062fd9 Support deleting feed 2021-02-21 10:29:07 +00:00
ab390c9d47 Added iframe toggle 2021-02-21 09:55:33 +00:00
6b58e9115b Improve handling of background fetch update 2021-02-21 09:33:37 +00:00
126820ade6 Better styling on code elements 2021-02-21 09:30:11 +00:00
122bfd666a Fixed created time nil 2021-02-21 09:09:25 +00:00
Marcus Noble
68e9418c12 Removed link to create new issue 2020-12-13 17:25:31 +00:00
43c6017397 Show feed list on mobile 2020-11-18 19:45:39 +00:00
f6ebdc480e Background refresh 2020-11-18 19:18:08 +00:00
c836fe576d Fix unread count 2020-11-18 19:12:31 +00:00
649595ed14 Fix posts with relative image/links 2020-11-18 19:02:31 +00:00
f7160f7a18 Added release task 2020-11-10 18:36:48 +00:00
33d91402fd Handle pending read with unread count 2020-11-10 15:58:02 +00:00
3f20bd1cd3 Make feeds linkable 2020-11-09 21:22:35 +00:00
ef02f13ef4 Filter out empty paragraphs 2020-11-09 21:19:15 +00:00
32 changed files with 8826 additions and 61 deletions

View File

@@ -59,7 +59,7 @@ ci:
.PHONY: release # Release the latest version of the application
release:
@echo "⚠️ 'released' unimplemented"
@kubectl --namespace rss set image deployment rss web=docker.cluster.fun/averagemarcus/gopherss:$(SHA)
.PHONY: help # Show this list of commands
help:

View File

@@ -8,7 +8,7 @@ RSS reader written in Go
## Contributing
If you find a bug or have an idea for a new feature please [raise an issue](https://github.com/averagemarcus/gopherss/issues/new) to discuss it.
If you find a bug or have an idea for a new feature please raise an issue to discuss it.
Pull requests are welcomed but please try and follow similar code style as the rest of the project and ensure all tests and code checkers are passing.

1
go.mod
View File

@@ -3,6 +3,7 @@ module github.com/averagemarcus/gopherss
go 1.15
require (
github.com/PuerkitoBio/goquery v1.5.1
github.com/dustin/go-humanize v1.0.0
github.com/gofiber/fiber/v2 v2.0.6
github.com/gofiber/template v1.6.3

9
go.sum
View File

@@ -140,6 +140,7 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -185,6 +186,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -201,6 +203,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kyoh86/exportloopref v0.1.4/go.mod h1:h1rDl2Kdj97+Kwh4gdz3ujE7XHmH51Q0lUiZ1z4NLj8=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -252,6 +255,7 @@ github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nishanths/exhaustive v0.0.0-20200525081945-8e46705b6132/go.mod h1:wBEpHwM2OdmeNpdCvRPUlkEbBuaFmcK4Wv8Q7FuGW3c=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -298,7 +302,9 @@ github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOms
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
@@ -327,6 +333,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@@ -522,6 +529,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -537,6 +545,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=

View File

@@ -3,9 +3,12 @@ package feeds
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/mmcdole/gofeed"
"github.com/spf13/viper"
)
@@ -31,12 +34,17 @@ func Refresh() error {
}
}
func RefreshFeed(url string) Feed {
fmt.Printf("Refreshing %s\n", url)
func RefreshFeed(feedUrl string) Feed {
fmt.Printf("Refreshing %s\n", feedUrl)
var feed Feed
f, err := fp.ParseURL(url)
if err != nil {
fmt.Printf("Failed to refresh %s\n", url)
f, err := fp.ParseURL(feedUrl)
if err != nil && err == gofeed.ErrFeedTypeNotDetected {
foundFeed := loadFeedFromWebpage(feedUrl)
if foundFeed != nil {
feed = *foundFeed
}
} else if err != nil {
fmt.Printf("Failed to refresh %s - %v\n", feedUrl, err)
} else {
imageURL := ""
if f.Image != nil {
@@ -44,11 +52,11 @@ func RefreshFeed(url string) Feed {
}
feed = Feed{
ID: strings.ReplaceAll(base64.StdEncoding.EncodeToString([]byte(url)), "/", ""),
ID: strings.ReplaceAll(base64.StdEncoding.EncodeToString([]byte(feedUrl)), "/", ""),
Title: f.Title,
Description: f.Description,
HomepageURL: f.Link,
FeedURL: url,
FeedURL: feedUrl,
ImageURL: imageURL,
LastUpdated: f.UpdatedParsed,
Items: []Item{},
@@ -59,6 +67,11 @@ func RefreshFeed(url string) Feed {
imageURL = f.Image.URL
}
createdTime := item.PublishedParsed
if createdTime == nil {
createdTime = item.UpdatedParsed
}
feed.Items = append(feed.Items, Item{
ID: strings.ReplaceAll(base64.StdEncoding.EncodeToString([]byte(item.GUID)), "/", ""),
Title: item.Title,
@@ -67,7 +80,7 @@ func RefreshFeed(url string) Feed {
URL: item.Link,
ImageURL: imageURL,
LastUpdated: item.UpdatedParsed,
Created: item.PublishedParsed,
Created: createdTime,
GUID: item.GUID,
FeedID: feed.ID,
})
@@ -79,3 +92,35 @@ func RefreshFeed(url string) Feed {
return feed
}
func loadFeedFromWebpage(webpageUrl string) *Feed {
res, err := http.Get(webpageUrl)
if err != nil {
fmt.Println(err)
return nil
}
defer res.Body.Close()
if res.StatusCode != 200 {
fmt.Printf("status code error: %d %s", res.StatusCode, res.Status)
return nil
}
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
fmt.Println(err)
return nil
}
feedUrl, ok := doc.Find(`[rel="alternate"][type="application/rss+xml"]`).First().Attr("href")
if ok {
if !strings.HasPrefix(feedUrl, "http") {
parsedUrl, _ := url.Parse(webpageUrl)
feedUrl = fmt.Sprintf("%s://%s%s", parsedUrl.Scheme, parsedUrl.Host, feedUrl)
}
feed := RefreshFeed(feedUrl)
return &feed
}
return nil
}

View File

@@ -33,6 +33,11 @@ func (fs *FeedStore) GetFeed(id string) *Feed {
return feed
}
func (fs *FeedStore) DeleteFeed(id string) {
fs.getDB().Delete(Feed{ID: id})
fs.getDB().Unscoped().Delete(Item{}, "feed_id = ?", id)
}
func (fs *FeedStore) GetItem(id string) *Item {
item := &Item{}
fs.getDB().Where("id = ?", id).First(item)

View File

@@ -17,6 +17,11 @@ func (a *API) GetFeed(c *fiber.Ctx) error {
return c.JSON(a.FeedStore.GetFeed(c.Params("id")))
}
func (a *API) DeleteFeed(c *fiber.Ctx) error {
a.FeedStore.DeleteFeed(c.Params("id"))
return nil
}
func (a *API) PostFeed(c *fiber.Ctx) error {
url := ""
if err := c.BodyParser(&url); err != nil {

View File

@@ -2,10 +2,7 @@ package server
import (
"fmt"
"html/template"
"time"
"github.com/dustin/go-humanize"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html"
@@ -24,21 +21,6 @@ func Start(port string) error {
engine := html.New("./views", ".html")
engine.Reload(true)
engine.AddFunc("htmlSafe", func(html string) template.HTML {
return template.HTML(html)
})
engine.AddFunc("humanDate", func(date time.Time) template.HTML {
return template.HTML(humanize.Time(date))
})
engine.AddFunc("coalesce", func(args ...*string) string {
for _, s := range args {
if s != nil && *s != "" {
return *s
}
}
return ""
})
app := fiber.New(fiber.Config{
Views: engine,
})
@@ -50,6 +32,7 @@ func Start(port string) error {
app.Get("/api/feeds", api.GetFeeds)
app.Post("/api/feeds", api.PostFeed)
app.Get("/api/feed/:id", api.GetFeed)
app.Delete("/api/feed/:id", api.DeleteFeed)
app.Get("/api/item/:id", api.GetItem)
app.Post("/api/item/:id/save", api.SaveItem)
app.Get("/api/unread", api.GetUnread)

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gopherss</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22></text><text text-anchor=%22end%22 y=%221.9em%22 x=%221.9em%22 font-size=%2250%22>10</text></svg>">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- <link rel="apple-touch-icon" href="static/icon.png"> -->
@@ -16,8 +17,10 @@
</script>
<script src="/static/feed-item.js" defer></script>
<script src="/static/favicon.js"></script>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-progressbar@0.7.5/dist/vue-progressbar.min.js"></script>
<script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script>
<script src="https://unpkg.com/dayjs@1.9.5/plugin/relativeTime.js"></script>
<script>dayjs.extend(window.dayjs_plugin_relativeTime)</script>
@@ -30,12 +33,17 @@
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="hack">
<div class="container">
<div id="app" class="container">
<header>
<button class="feed-toggle" v-on:click="toggleFeeds">
<svg width="24" height="24" viewbox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24 16a3.5 3.5 0 110-7 3.5 3.5 0 010 7z" fill="#212121"/><path d="M24 27.5a3.5 3.5 0 110-7 3.5 3.5 0 010 7z" fill="#212121"/><path d="M20.5 35.5a3.5 3.5 0 107 0 3.5 3.5 0 00-7 0z" fill="#212121"/></svg>
</button>
<h1 class="title">
Gopherss
</h1>
</header>
<div id="app">
<div>
<div class="menu">
<button title="Show Read" v-on:click="toggleShowRead()">
<svg width="30" height="30" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a4 4 0 110 8 4 4 0 010-8zm0-3.5a10 10 0 019.7 7.6.8.8 0 01-1.5.3 8.5 8.5 0 00-16.4 0 .8.8 0 01-1.5-.3A10 10 0 0112 5.5z" :style="{'fill': showRead ? '#ff2e88' : '' }" fill-rule="nonzero"/></svg>
@@ -56,8 +64,13 @@
All ({{unread}})
</div>
<div v-for="feed in feeds" :class="{strong: unreadCounts[feed.ID], 'alert': true, 'alert-success': selectedFeed == feed.ID }" :data-feed="feed.FeedURL" v-on:click="loadFeed(feed.ID)">
<div v-for="feed in sortedFeeds" :class="{strong: unreadCounts[feed.ID], 'alert': true, 'alert-success': selectedFeed == feed.ID }" :data-feed="feed.FeedURL" v-on:click="loadFeed(feed.ID)">
<img :src="feedIcon(feed)" style="height: 16px; width: 16px;" onerror="this.style.visibility = 'hidden'" /> {{feed.Title}} ({{unreadCounts[feed.ID] || '0'}})
<div style="float:right">
<button title="Delete Feed" v-on:click="deleteFeed(feed)" :disabled="isBusy">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" width="16px" viewBox="0 0 473 473" enable-background="new 0 0 473 473"><g><path d="M317.7 214.4l5.6-86.4h21V38h-98.4V0H132.7v38H34.3v90h21l20 305h140.5a129.6 129.6 0 00223-89.4c0-68.6-53.7-124.8-121.1-129.2zM162.7 30h53.2v8h-53.2v-8zM64.3 68h250v30h-250V68zm39 335l-18-275h208l-5.8 88a129.6 129.6 0 00-93.2 187h-91zm206 40a99.5 99.5 0 010-198.9 99.5 99.5 0 010 198.9z"/><path d="M342.2 289.4l-33 33-32.9-33-21.2 21.2 33 33-33 33 21.2 21.1 33-33 33 33 21.2-21.2-33-33 33-32.9z"/></g><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/></svg>
</button>
</div>
</div>
<div :class="{ strong: items.length, alert: true, 'alert-success': selectedFeed == 'SAVED'}" v-on:click="loadFeed('SAVED')">
@@ -78,7 +91,6 @@
<fieldset class="form-group">
<label for="url">URL:</label>
<input id="url" type="text" placeholder="" class="form-control" v-model="newSiteURL">
<div class="help-block">Enter the direct URL to the feed</div>
</fieldset>
<div class="form-actions">
<button type="button" class="btn btn-primary btn-block" v-on:click="addSite(newSiteURL)" :disabled="isBusy">Add</button>
@@ -106,6 +118,9 @@
<div class="card item-content" :data-id="item.ID" v-if="item.ID == selectedItem">
<div class="card-content">
<div class="menu">
<button title="Show IFrame" v-on:click="showIframe(item)" :disabled="isBusy">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 426 426" ><path d="M406.8 54.2H19.2A19.2 19.2 0 000 73.4v279.2c0 10.6 8.6 19.2 19.2 19.2h387.6c10.6 0 19.2-8.6 19.2-19.2V73.4c0-10.6-8.6-19.2-19.2-19.2zM368.4 82a17.8 17.8 0 110 35.7 17.8 17.8 0 010-35.7zm-48 0a17.8 17.8 0 110 35.7 17.8 17.8 0 010-35.7zm-48 0a17.8 17.8 0 110 35.7 17.8 17.8 0 010-35.7zm115.2 251.5H38.4V141.6h349.2v191.8z" :style="{'fill': item.IframeVisible ? '#ff2e88' : '' }" fill-rule="nonzero"/></svg>
</button>
<button title="Save" v-on:click="saveItem(item)" :disabled="isBusy">
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M12.8 5.6l-.8.8-.8-.8a5.4 5.4 0 00-7.6 7.6l7.9 7.9c.3.3.7.3 1 0l8-8a5.4 5.4 0 10-7.7-7.5z" :style="{'fill': item.Save ? '#ff2e88' : '' }" fill-rule="nonzero"/></svg>
</button>
@@ -122,10 +137,34 @@
</div>
</div>
<vue-progress-bar></vue-progress-bar>
</div>
<script>
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
Vue.use(VueProgressBar, {
color: '#00bcd4',
failedColor: '#ff2e88',
thickness: '5px',
transition: {
speed: '1s',
opacity: '0.8s',
termination: 100
},
autoRevert: true,
location: 'top',
inverse: false
});
const vm = new Vue({
el: '#app',
data: {
@@ -151,14 +190,17 @@
return this.items.filter(item => item.ID == this.selectedItem || item.FeedID === this.selectedFeed && (!item.Read || item.Read === this.showRead));
}
},
sortedFeeds() {
return this.feeds.sort((a, b) => a.Title.toLowerCase() > b.Title.toLowerCase());
},
unread() {
return this.items.filter(item => !item.Read).length;
return this.items.filter(item => !item.Read && !item.PendingRead).length;
},
saved() {
return this.savedItems.length;
},
unreadCounts() {
return this.items.filter(item => !item.Read).reduce((acc, item) => {
return this.items.filter(item => !item.Read && !item.PendingRead).reduce((acc, item) => {
if (!acc[item.FeedID]) acc[item.FeedID] = 0;
acc[item.FeedID]++;
return acc;
@@ -168,8 +210,14 @@
methods: {
setPageTitle() {
document.title = `Gopherss (${this.unread})`;
setFavicon(this.unread);
},
setBusy(isBusy) {
if (isBusy) {
this.$Progress.start();
} else {
this.$Progress.finish();
}
this.isBusy = isBusy;
document.body.style.cursor = isBusy ? "wait" : "";
this.setPageTitle();
@@ -181,18 +229,26 @@
},
loadFeed(feed) {
this.selectedItem = undefined;
this.items.forEach(item => item.Read = item.Read || item.PendingRead);
this.items.forEach(item => {
item.Read = item.Read || item.PendingRead;
item.IframeVisible = false;
});
this.selectedFeed = feed;
window.location.hash = feed;
},
loadItem(item) {
this.setBusy(true);
if (this.selectedItem === item.ID) {
this.selectedItem = undefined;
} else {
this.selectedItem = item.ID;
document.getElementById(this.selectedItem).scrollIntoView();
if (!isInViewport(document.getElementById(this.selectedItem))) {
document.getElementById(this.selectedItem).scrollIntoView();
}
item.PendingRead = true;
fetch(`/api/read/${item.ID}`, {method: "POST"})
}
this.setBusy(false);
},
saveItem(item) {
this.setBusy(true);
@@ -208,6 +264,21 @@
this.setBusy(false);
})
},
showIframe(item) {
item.IframeVisible = !item.IframeVisible;
document.querySelector(`feed-item[item-id='${item.ID}'`).showIframe();
},
deleteFeed(feed) {
if (confirm(`Are you sure you want to remove '${feed.Title}'`)) {
this.setBusy(true);
fetch(`/api/feed/${feed.ID}`, {method: "DELETE"})
.then(() => {
this.feeds = this.feeds.filter(f => f.ID != feed.ID);
this.items = this.items.filter(i => i.FeedID != feed.ID);
this.setBusy(false);
});
}
},
nextItem() {
let currentItem = -1;
if (this.selectedItem != undefined) {
@@ -270,10 +341,12 @@
})
.then(() => {
this.setBusy(false);
this.newSiteURL = '';
})
.catch(err => {
console.error(err);
this.setBusy(false);
this.newSiteURL = '';
});
this.showAddModal = false;
},
@@ -331,17 +404,23 @@
}
return "https://s2.googleusercontent.com/s2/favicons?domain_url=" + (feed.HomepageURL || feed.FeedURL);
},
toggleFeeds() {
document.querySelector('.feeds').classList.toggle('show-mobile');
}
},
created() {
this.setBusy(true);
Promise.all([
fetch(`/api/feeds`).then(res => res.json()).then(feeds => this.feeds = feeds),
fetch(`/api/unread`).then(res => res.json()).then(items => this.items = items),
fetch(`/api/unread`).then(res => res.json()).then(items => this.items = items.map(item => {item.PendingRead = false; item.IframeVisible = false; return item;})),
fetch(`/api/saved`).then(res => res.json()).then(items => this.savedItems = items)
])
.then(() => {
this.setBusy(false);
if (window.location.hash.length > 1) {
this.loadFeed(window.location.hash.substr(1));
}
})
.catch(err => {
console.error(err);
@@ -371,6 +450,41 @@
}
};
let inView = true;
window.onfocus = window.onblur = window.onpageshow = window.onpagehide = function (e) {
if ({focus:1, pageshow:1}[e.type]) {
if (inView) return;
inView = true;
} else if (inView) {
inView = false;
}
};
// Fetch updates every 5 minutes
setInterval(() => {
if (!inView) {
fetch(`/api/unread`)
.then(res => res.json())
.then(items => {
if (!this.showRead) {
if (this.selectedItem && !items.some(i => i.ID == this.selectedItem)) {
items.unshift(this.items.find(i => i.ID == this.selectedItem));
}
this.items = items;
} else {
for (let item of items) {
if (!this.items.some(i => i.ID == item.ID)) {
this.items.unshift(item);
}
}
}
this.setPageTitle();
});
}
}, 5 * 60 * 1000);
document.addEventListener('keydown', this._keyListener.bind(this));
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {

49
views/static/favicon.js Normal file
View File

@@ -0,0 +1,49 @@
const link = document.querySelector('link[rel="icon"]');
function setFavicon(count) {
const padding=100/16;
const svg = document. createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const t1 = document. createElementNS("http://www.w3.org/2000/svg", "text");
t1.setAttribute('y', '.9em');
t1.setAttribute('font-size', '90');
if (count == 0) {
t1.textContent = '📭';
} else {
t1.textContent = '📬';
}
svg.appendChild(t1);
if (count) {
const t2 = document. createElementNS("http://www.w3.org/2000/svg", "text");
t2.setAttribute('x', 100 - padding);
t2.setAttribute('y', 100 - padding);
t2.setAttribute('font-size', '60');
t2.setAttribute('text-anchor', 'end');
t2.setAttribute('alignment-baseline', 'text-bottom');
t2.setAttribute('fill', 'white');
t2.style.font = 'sans';
t2.style.fontWeight = '400';
t2.textContent = count;
svg.appendChild(t2);
// measure the text
document.body.appendChild(svg);
const rect = t2.getBBox();
document.body.removeChild(svg);
const r1 = document. createElementNS("http://www.w3.org/2000/svg", "rect");
r1.setAttribute('x', rect.x);
r1.setAttribute('y', rect.y);
r1.setAttribute('width', rect.width + padding);
r1.setAttribute('height', rect.height + padding);
r1.setAttribute('rx', padding);
r1.setAttribute('ry', padding);
r1.style.fill = 'red';
svg.appendChild(r1);
svg.appendChild(t2);
}
link.href='data:image/svg+xml,' + svg.outerHTML.replace(/"/ig, '%22');
}

View File

@@ -9,13 +9,6 @@ class FeedItem extends HTMLElement {
const template = document.createElement('template');
template.innerHTML = `
<style>
@font-face {
font-family: "charter";
src: url("https://glyph.medium.com/font/be78681/0-3j_4g_6bu_6c4_6c8_6c9_6cc_6cd_6ci_6cm/charter-400-normal.woff") format("woff");
font-style: normal;
font-weight: 400;
unicode-range: U+0-7F, U+A0, U+200A, U+2014, U+2018, U+2019, U+201C, U+201D, U+2022, U+2026;
}
:host {
width: 100% !important;
@@ -35,38 +28,114 @@ class FeedItem extends HTMLElement {
img {
margin: auto auto !important;
}
p {
font-family: charter, Georgia, "Times New Roman", Times, serif;
h1, h2, h3, h4 {
font-family: "Atkinson Hyperlegible Bold";
margin-top: 1.3em;
line-height: 1em;
}
:root > h1 {
margin-top: 0;
}
p, a {
line-height: 1.2em;
}
p, li, div {
font-family: "Atkinson Hyperlegible Regular";
font-style: normal;
font-weight: 400;
letter-spacing: -0.063px;
line-height: 32px
letter-spacing: 0.05em
}
em {
font-family: "Atkinson Hyperlegible Italic";
font-style: normal;
}
strong {
font-weight: 500;
font-family: "Atkinson Hyperlegible Bold";
}
em strong, strong em {
font-family: "Atkinson Hyperlegible BoldItalic";
}
li {
margin: 0.6em 0;
}
a {
color: #333;
font-weight: bold;
font-family: "Atkinson Hyperlegible Bold";
font-weight: 500;
letter-spacing: 0.05em
}
:host(.dark) a {
color: #ccc;
color: #eee;
}
a:hover, :host(.dark) a:hover {
color: #ff2e88;
}
pre {
overflow-x: scroll;
padding: 8px;
background: #62848463;
}
pre code {
margin-right: 8px;
}
p code {
background: #62848463;
padding: 0 4px;
}
iframe {
display: block;
width: 100%;
min-height: 600px;
border: none;
}
</style>
`;
fetch(`/api/item/${this.getAttribute('item-id')}`)
.then(res => res.json())
.then(item => {
template.innerHTML += item.Content || item.Description;
template.innerHTML += `<h1><a href="${item.URL}" target="_blank" rel="noopener">${item.Title}</a></h1>`;
template.innerHTML += `<div class="feedContent">${item.Content || item.Description}</div>`;
template.innerHTML += `<iframe style="display: none;" data-src="${item.URL}"></iframe>`
this.shadowRoot.appendChild(template.content.cloneNode(true));
[...this.shadowRoot.querySelectorAll('a[href^=http]')].forEach(a => {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
})
})
});
[...this.shadowRoot.querySelectorAll('p')].forEach(p => {
if (p.innerText.trim() == "") {
p.remove();
}
});
let url = new URL(item.URL);
[...this.shadowRoot.querySelectorAll('img[src^="/"]')].forEach(i => {
i.src = url.origin + i.getAttribute('src');
});
[...this.shadowRoot.querySelectorAll('a[href^="/"]')].forEach(a => {
a.href = url.origin + a.getAttribute('href');
});
[...this.shadowRoot.querySelectorAll('img:not([src^=http])')].forEach(i => {
i.src = url.origin +'/'+ i.getAttribute('src');
});
[...this.shadowRoot.querySelectorAll('a:not([href^=http])')].forEach(a => {
a.href = url.origin +'/'+ a.getAttribute('href');
});
})
}
showIframe() {
if (this.shadowRoot.querySelector(".feedContent").style.display != "none") {
this.shadowRoot.querySelector(".feedContent").style.display = "none";
this.shadowRoot.querySelector("iframe").src = this.shadowRoot.querySelector("iframe").dataset.src;
this.shadowRoot.querySelector("iframe").style.display = "block";
} else {
this.shadowRoot.querySelector(".feedContent").style.display = "block";
this.shadowRoot.querySelector("iframe").style.display = "none";
}
}
}
customElements.define('feed-item', FeedItem);

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -1,3 +1,48 @@
@font-face {
font-family: "Atkinson Hyperlegible Bold";
font-style: normal;
font-weight: normal;
font-display: block;
src: url("/static/fonts/Atkinson-Hyperlegible-Bold-102a.woff2") format("woff2"),
url("/static/fonts/Atkinson-Hyperlegible-Bold-102a.woff") format("woff"),
url("/static/fonts/Atkinson-Hyperlegible-Bold-102a.ttf") format("ttf"),
url("/static/fonts/Atkinson-Hyperlegible-Bold-102a.eot") format("eot"),
url("/static/fonts/Atkinson-Hyperlegible-Bold-102a.svg") format("svg");
}
@font-face {
font-family: "Atkinson Hyperlegible Regular";
font-style: normal;
font-weight: normal;
font-display: block;
src: url("/static/fonts/Atkinson-Hyperlegible-Regular-102a.woff2") format("woff2"),
url("/static/fonts/Atkinson-Hyperlegible-Regular-102a.woff") format("woff"),
url("/static/fonts/Atkinson-Hyperlegible-Regular-102a.ttf") format("ttf"),
url("/static/fonts/Atkinson-Hyperlegible-Regular-102a.eot") format("eot"),
url("/static/fonts/Atkinson-Hyperlegible-Regular-102a.svg") format("svg");
}
@font-face {
font-family: "Atkinson Hyperlegible Italic";
font-style: normal;
font-weight: normal;
font-display: block;
src: url("/static/fonts/Atkinson-Hyperlegible-Italic-102a.woff2") format("woff2"),
url("/static/fonts/Atkinson-Hyperlegible-Italic-102a.woff") format("woff"),
url("/static/fonts/Atkinson-Hyperlegible-Italic-102a.ttf") format("ttf"),
url("/static/fonts/Atkinson-Hyperlegible-Italic-102a.eot") format("eot"),
url("/static/fonts/Atkinson-Hyperlegible-Italic-102a.svg") format("svg");
}
@font-face {
font-family: "Atkinson Hyperlegible BoldItalic";
font-style: normal;
font-weight: normal;
font-display: block;
src: url("/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.woff2") format("woff2"),
url("/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.woff") format("woff"),
url("/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.ttf") format("ttf"),
url("/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.eot") format("eot"),
url("/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.svg") format("svg");
}
body {
padding-bottom: 20px;
}
@@ -25,6 +70,14 @@ body {
position: relative;
}
header {
display: flex;
}
header h1 {
width: -moz-available;
}
.item-heading .item-title {
margin-bottom: 0;
@@ -82,17 +135,17 @@ body {
text-align: right;
}
.menu button {
button {
background: none;
border: none;
cursor: pointer;
}
.menu button:disabled {
button:disabled {
cursor: not-allowed;
}
.menu button:not(:disabled):hover svg path {
button:not(:disabled):hover svg path {
fill: #ff2e88;
}
@@ -111,13 +164,31 @@ body {
box-shadow: 1px 2px 3px #333
}
@media only screen and (min-width: 701px) {
.feed-toggle {
display: none;
}
}
@media only screen and (max-width: 700px) {
.feeds{ display: none !important; }
.feeds{
position: initial;
width: 100%;
height: 0px;
margin: 0;
overflow: hidden;
transition: all 2s;
}
.container {
max-width: 100em;
}
}
.feeds.show-mobile {
height: 50vh;
overflow-y: scroll;
}
.dark {
background: #333;
}
@@ -145,7 +216,7 @@ body {
}
.item-content .card-content p {
font-family: 'Roboto', sans-serif;
font-family: "Atkinson Hyperlegible Regular"; /*'Roboto', sans-serif;*/
font-size: 14px;
line-height: 20px;
letter-spacing: 0em;
@@ -156,6 +227,10 @@ body {
color: #333;
font-weight: bold;
}
.dark .item-content .card-content a {
color: #ccc;
.dark {
color: #eee;
}
.card-header {
padding: 5px !important;
}