Compare commits
31 Commits
d5d24b0e7e
...
master
Author | SHA1 | Date | |
---|---|---|---|
8814467db9 | |||
caa0b01937 | |||
ea8c2e4bbb | |||
7613116bf3 | |||
a21dd6b74f | |||
f5b386f70d | |||
da1319ca68 | |||
9306e6cbe6 | |||
9f4e07d5a5 | |||
42014bdaa1 | |||
47c72d0029 | |||
761b4f05fd | |||
9c07dd226e | |||
b2e70bb0f7 | |||
00eeef7ec6 | |||
a96a43f9ec | |||
1fc26bfdfe | |||
22db062fd9 | |||
ab390c9d47 | |||
6b58e9115b | |||
126820ade6 | |||
122bfd666a | |||
|
68e9418c12 | ||
43c6017397 | |||
f6ebdc480e | |||
c836fe576d | |||
649595ed14 | |||
f7160f7a18 | |||
33d91402fd | |||
3f20bd1cd3 | |||
ef02f13ef4 |
2
Makefile
2
Makefile
@@ -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:
|
||||
|
@@ -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
1
go.mod
@@ -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
9
go.sum
@@ -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=
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
134
views/index.html
134
views/index.html
@@ -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
49
views/static/favicon.js
Normal 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');
|
||||
}
|
@@ -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);
|
||||
|
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.eot
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.eot
Normal file
Binary file not shown.
2300
views/static/fonts/Atkinson-Hyperlegible-Bold-102.svg
Normal file
2300
views/static/fonts/Atkinson-Hyperlegible-Bold-102.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 169 KiB |
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.ttf
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.ttf
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.woff
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102.woff
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102a.woff2
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Bold-102a.woff2
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.eot
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.eot
Normal file
Binary file not shown.
2303
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.svg
Normal file
2303
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 172 KiB |
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.ttf
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.ttf
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.woff
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102.woff
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.woff2
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-BoldItalic-102a.woff2
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.eot
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.eot
Normal file
Binary file not shown.
1912
views/static/fonts/Atkinson-Hyperlegible-Italic-102.svg
Normal file
1912
views/static/fonts/Atkinson-Hyperlegible-Italic-102.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 138 KiB |
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.ttf
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.ttf
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.woff
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102.woff
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102a.woff2
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Italic-102a.woff2
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.eot
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.eot
Normal file
Binary file not shown.
1895
views/static/fonts/Atkinson-Hyperlegible-Regular-102.svg
Normal file
1895
views/static/fonts/Atkinson-Hyperlegible-Regular-102.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 136 KiB |
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.ttf
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.ttf
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.woff
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102.woff
Normal file
Binary file not shown.
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102a.woff2
Normal file
BIN
views/static/fonts/Atkinson-Hyperlegible-Regular-102a.woff2
Normal file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user