aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2017-12-10 19:01:38 -0800
committerGravatar Frédéric Guillot <fred@miniflux.net>2017-12-10 19:01:38 -0800
commit7a35c58f53d76356292e3e0ca9c91add3595a9e0 (patch)
tree99d2720d78049751d18033cb5ccafedc91f596c2
parentb75a9987ba99047efe846b8f196bc5a28b7474c1 (diff)
Add readability package to fetch original content
-rw-r--r--locale/translations.go7
-rw-r--r--locale/translations/fr_FR.json3
-rw-r--r--reader/readability/readability.go306
-rw-r--r--reader/scraper/scraper.go38
-rw-r--r--server/routes.go1
-rw-r--r--server/static/bin.go2
-rw-r--r--server/static/css.go6
-rw-r--r--server/static/css/common.css8
-rw-r--r--server/static/js.go11
-rw-r--r--server/static/js/app.js35
-rw-r--r--server/template/common.go2
-rw-r--r--server/template/html/entry.html27
-rw-r--r--server/template/views.go31
-rw-r--r--server/ui/controller/entry.go79
-rw-r--r--server/ui/controller/integrations.go40
-rw-r--r--sql/sql.go2
-rw-r--r--storage/entry.go17
17 files changed, 545 insertions, 70 deletions
diff --git a/locale/translations.go b/locale/translations.go
index f0ac381..217541e 100644
--- a/locale/translations.go
+++ b/locale/translations.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-04 21:22:23.44799753 -0800 PST m=+0.006646042
+// 2017-12-10 18:56:24.387844114 -0800 PST m=+0.029823201
package locale
@@ -166,12 +166,13 @@ var translations = map[string]string{
"Instapaper Password": "Mot de passe Instapaper",
"Activate Fever API": "Activer l'API de Fever",
"Fever Username": "Nom d'utilisateur pour l'API de Fever",
- "Fever Password": "Mot de passe pour l'API de Fever"
+ "Fever Password": "Mot de passe pour l'API de Fever",
+ "Fetch original content": "Récupérer le contenu original"
}
`,
}
var translationsChecksums = map[string]string{
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
- "fr_FR": "ef3d095f3e78d88a2746240769fa30d2e83c6519187d98e2193c9231dda5d882",
+ "fr_FR": "fd629b171aefa50dd0a6100acaac8fbecbdf1a1d53e3fce984234565ec5bb5d5",
}
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index 17e69cc..cc82efe 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -150,5 +150,6 @@
"Instapaper Password": "Mot de passe Instapaper",
"Activate Fever API": "Activer l'API de Fever",
"Fever Username": "Nom d'utilisateur pour l'API de Fever",
- "Fever Password": "Mot de passe pour l'API de Fever"
+ "Fever Password": "Mot de passe pour l'API de Fever",
+ "Fetch original content": "Récupérer le contenu original"
}
diff --git a/reader/readability/readability.go b/reader/readability/readability.go
new file mode 100644
index 0000000..37b4813
--- /dev/null
+++ b/reader/readability/readability.go
@@ -0,0 +1,306 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package readability
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "math"
+ "regexp"
+ "strings"
+
+ "github.com/PuerkitoBio/goquery"
+ "golang.org/x/net/html"
+)
+
+const (
+ defaultTagsToScore = "section,h2,h3,h4,h5,h6,p,td,pre,div"
+)
+
+var (
+ divToPElementsRegexp = regexp.MustCompile(`(?i)<(a|blockquote|dl|div|img|ol|p|pre|table|ul)`)
+ sentenceRegexp = regexp.MustCompile(`\.( |$)`)
+
+ blacklistCandidatesRegexp = regexp.MustCompile(`(?i)popupbody|-ad|g-plus`)
+ okMaybeItsACandidateRegexp = regexp.MustCompile(`(?i)and|article|body|column|main|shadow`)
+ unlikelyCandidatesRegexp = regexp.MustCompile(`(?i)banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|modal|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote`)
+
+ negativeRegexp = regexp.MustCompile(`(?i)hidden|^hid$|hid$|hid|^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget|byline|author|dateline|writtenby|p-author`)
+ positiveRegexp = regexp.MustCompile(`(?i)article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story`)
+)
+
+type candidate struct {
+ selection *goquery.Selection
+ score float32
+}
+
+func (c *candidate) Node() *html.Node {
+ return c.selection.Get(0)
+}
+
+func (c *candidate) String() string {
+ id, _ := c.selection.Attr("id")
+ class, _ := c.selection.Attr("class")
+
+ if id != "" && class != "" {
+ return fmt.Sprintf("%s#%s.%s => %f", c.Node().DataAtom, id, class, c.score)
+ } else if id != "" {
+ return fmt.Sprintf("%s#%s => %f", c.Node().DataAtom, id, c.score)
+ } else if class != "" {
+ return fmt.Sprintf("%s.%s => %f", c.Node().DataAtom, class, c.score)
+ }
+
+ return fmt.Sprintf("%s => %f", c.Node().DataAtom, c.score)
+}
+
+type candidateList map[*html.Node]*candidate
+
+func (c candidateList) String() string {
+ var output []string
+ for _, candidate := range c {
+ output = append(output, candidate.String())
+ }
+
+ return strings.Join(output, ", ")
+}
+
+// ExtractContent returns relevant content.
+func ExtractContent(page io.Reader) (string, error) {
+ document, err := goquery.NewDocumentFromReader(page)
+ if err != nil {
+ return "", err
+ }
+
+ document.Find("script,style,noscript").Each(func(i int, s *goquery.Selection) {
+ removeNodes(s)
+ })
+
+ transformMisusedDivsIntoParagraphs(document)
+ removeUnlikelyCandidates(document)
+
+ candidates := getCandidates(document)
+ log.Println("Candidates:", candidates)
+
+ topCandidate := getTopCandidate(document, candidates)
+ log.Println("TopCandidate:", topCandidate)
+
+ output := getArticle(topCandidate, candidates)
+ return output, nil
+}
+
+// Now that we have the top candidate, look through its siblings for content that might also be related.
+// Things like preambles, content split by ads that we removed, etc.
+func getArticle(topCandidate *candidate, candidates candidateList) string {
+ output := bytes.NewBufferString("<div>")
+ siblingScoreThreshold := float32(math.Max(10, float64(topCandidate.score*.2)))
+
+ topCandidate.selection.Siblings().Union(topCandidate.selection).Each(func(i int, s *goquery.Selection) {
+ append := false
+ node := s.Get(0)
+
+ if node == topCandidate.Node() {
+ append = true
+ } else if c, ok := candidates[node]; ok && c.score >= siblingScoreThreshold {
+ append = true
+ }
+
+ if s.Is("p") {
+ linkDensity := getLinkDensity(s)
+ content := s.Text()
+ contentLength := len(content)
+
+ if contentLength >= 80 && linkDensity < .25 {
+ append = true
+ } else if contentLength < 80 && linkDensity == 0 && sentenceRegexp.MatchString(content) {
+ append = true
+ }
+ }
+
+ if append {
+ tag := "div"
+ if s.Is("p") {
+ tag = node.Data
+ }
+
+ html, _ := s.Html()
+ fmt.Fprintf(output, "<%s>%s</%s>", tag, html, tag)
+ }
+ })
+
+ output.Write([]byte("</div>"))
+ return output.String()
+}
+
+func removeUnlikelyCandidates(document *goquery.Document) {
+ document.Find("*").Not("html,body").Each(func(i int, s *goquery.Selection) {
+ class, _ := s.Attr("class")
+ id, _ := s.Attr("id")
+ str := class + id
+
+ if blacklistCandidatesRegexp.MatchString(str) || (unlikelyCandidatesRegexp.MatchString(str) && !okMaybeItsACandidateRegexp.MatchString(str)) {
+ // log.Printf("Removing unlikely candidate - %s\n", str)
+ removeNodes(s)
+ }
+ })
+}
+
+func getTopCandidate(document *goquery.Document, candidates candidateList) *candidate {
+ var best *candidate
+
+ for _, c := range candidates {
+ if best == nil {
+ best = c
+ } else if best.score < c.score {
+ best = c
+ }
+ }
+
+ if best == nil {
+ best = &candidate{document.Find("body"), 0}
+ }
+
+ return best
+}
+
+// Loop through all paragraphs, and assign a score to them based on how content-y they look.
+// Then add their score to their parent node.
+// A score is determined by things like number of commas, class names, etc.
+// Maybe eventually link density.
+func getCandidates(document *goquery.Document) candidateList {
+ candidates := make(candidateList)
+
+ document.Find(defaultTagsToScore).Each(func(i int, s *goquery.Selection) {
+ text := s.Text()
+
+ // If this paragraph is less than 25 characters, don't even count it.
+ if len(text) < 25 {
+ return
+ }
+
+ parent := s.Parent()
+ parentNode := parent.Get(0)
+
+ grandParent := parent.Parent()
+ var grandParentNode *html.Node
+ if grandParent.Length() > 0 {
+ grandParentNode = grandParent.Get(0)
+ }
+
+ if _, found := candidates[parentNode]; !found {
+ candidates[parentNode] = scoreNode(parent)
+ }
+
+ if grandParentNode != nil {
+ if _, found := candidates[grandParentNode]; !found {
+ candidates[grandParentNode] = scoreNode(grandParent)
+ }
+ }
+
+ // Add a point for the paragraph itself as a base.
+ contentScore := float32(1.0)
+
+ // Add points for any commas within this paragraph.
+ contentScore += float32(strings.Count(text, ",") + 1)
+
+ // For every 100 characters in this paragraph, add another point. Up to 3 points.
+ contentScore += float32(math.Min(float64(int(len(text)/100.0)), 3))
+
+ candidates[parentNode].score += contentScore
+ if grandParentNode != nil {
+ candidates[grandParentNode].score += contentScore / 2.0
+ }
+ })
+
+ // Scale the final candidates score based on link density. Good content
+ // should have a relatively small link density (5% or less) and be mostly
+ // unaffected by this operation
+ for _, candidate := range candidates {
+ candidate.score = candidate.score * (1 - getLinkDensity(candidate.selection))
+ }
+
+ return candidates
+}
+
+func scoreNode(s *goquery.Selection) *candidate {
+ c := &candidate{selection: s, score: 0}
+
+ switch s.Get(0).DataAtom.String() {
+ case "div":
+ c.score += 5
+ case "pre", "td", "blockquote", "img":
+ c.score += 3
+ case "address", "ol", "ul", "dl", "dd", "dt", "li", "form":
+ c.score -= 3
+ case "h1", "h2", "h3", "h4", "h5", "h6", "th":
+ c.score -= 5
+ }
+
+ c.score += getClassWeight(s)
+ return c
+}
+
+// Get the density of links as a percentage of the content
+// This is the amount of text that is inside a link divided by the total text in the node.
+func getLinkDensity(s *goquery.Selection) float32 {
+ linkLength := len(s.Find("a").Text())
+ textLength := len(s.Text())
+
+ if textLength == 0 {
+ return 0
+ }
+
+ return float32(linkLength) / float32(textLength)
+}
+
+// Get an elements class/id weight. Uses regular expressions to tell if this
+// element looks good or bad.
+func getClassWeight(s *goquery.Selection) float32 {
+ weight := 0
+ class, _ := s.Attr("class")
+ id, _ := s.Attr("id")
+
+ if class != "" {
+ if negativeRegexp.MatchString(class) {
+ weight -= 25
+ }
+
+ if positiveRegexp.MatchString(class) {
+ weight += 25
+ }
+ }
+
+ if id != "" {
+ if negativeRegexp.MatchString(id) {
+ weight -= 25
+ }
+
+ if positiveRegexp.MatchString(id) {
+ weight += 25
+ }
+ }
+
+ return float32(weight)
+}
+
+func transformMisusedDivsIntoParagraphs(document *goquery.Document) {
+ document.Find("div").Each(func(i int, s *goquery.Selection) {
+ html, _ := s.Html()
+ if !divToPElementsRegexp.MatchString(html) {
+ node := s.Get(0)
+ node.Data = "p"
+ }
+ })
+}
+
+func removeNodes(s *goquery.Selection) {
+ s.Each(func(i int, s *goquery.Selection) {
+ parent := s.Parent()
+ if parent.Length() > 0 {
+ parent.Get(0).RemoveChild(s.Get(0))
+ }
+ })
+}
diff --git a/reader/scraper/scraper.go b/reader/scraper/scraper.go
new file mode 100644
index 0000000..6c51862
--- /dev/null
+++ b/reader/scraper/scraper.go
@@ -0,0 +1,38 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package scraper
+
+import (
+ "errors"
+
+ "github.com/miniflux/miniflux2/http"
+ "github.com/miniflux/miniflux2/reader/readability"
+ "github.com/miniflux/miniflux2/reader/sanitizer"
+)
+
+// Fetch download a web page a returns relevant contents.
+func Fetch(websiteURL string) (string, error) {
+ client := http.NewClient(websiteURL)
+ response, err := client.Get()
+ if err != nil {
+ return "", err
+ }
+
+ if response.HasServerFailure() {
+ return "", errors.New("unable to download web page")
+ }
+
+ page, err := response.NormalizeBodyEncoding()
+ if err != nil {
+ return "", err
+ }
+
+ content, err := readability.ExtractContent(page)
+ if err != nil {
+ return "", err
+ }
+
+ return sanitizer.Sanitize(websiteURL, content), nil
+}
diff --git a/server/routes.go b/server/routes.go
index a9fc7e8..2549e0f 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -100,6 +100,7 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
+ router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
diff --git a/server/static/bin.go b/server/static/bin.go
index 1f3a993..29664d8 100644
--- a/server/static/bin.go
+++ b/server/static/bin.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-03 17:25:29.40151375 -0800 PST m=+0.014540675
+// 2017-12-10 18:56:24.36887959 -0800 PST m=+0.010858677
package static
diff --git a/server/static/css.go b/server/static/css.go
index ff8a0cd..dd30930 100644
--- a/server/static/css.go
+++ b/server/static/css.go
@@ -1,14 +1,14 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-03 17:45:45.545527833 -0800 PST m=+0.018807123
+// 2017-12-10 18:56:24.370410193 -0800 PST m=+0.012389280
package static
var Stylesheets = map[string]string{
"black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555;color:#9b9b9b}.unread-counter{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-enclosure{border-color:#333}`,
- "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input[type=checkbox]{margin-bottom:15px}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}.form-section{border-left:2px dotted #ddd;padding-left:20px;margin-left:10px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#fcfcfc;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-actions{margin-bottom:20px}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em;overflow-wrap:break-word}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
+ "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input[type=checkbox]{margin-bottom:15px}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}.form-section{border-left:2px dotted #ddd;padding-left:20px;margin-left:10px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#fcfcfc;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-actions{margin-bottom:20px}.entry-actions li{display:inline}.entry-actions li:not(:last-child):after{content:"|"}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em;overflow-wrap:break-word}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
}
var StylesheetsChecksums = map[string]string{
"black": "a4d90d8c24bed43c38ebfa1054f7f5623423c16005843637bec59b3d98b0bcba",
- "common": "d7aab6c60af7f018e0bca5919bd09be24f700c23a42d38e318ed687c1e9c2daf",
+ "common": "79ae432118e1d80ef05829893767ded50572a8a616f150a7b63caae658497da2",
}
diff --git a/server/static/css/common.css b/server/static/css/common.css
index 759dbc4..4539eb7 100644
--- a/server/static/css/common.css
+++ b/server/static/css/common.css
@@ -511,6 +511,14 @@ a.button {
margin-bottom: 20px;
}
+.entry-actions li {
+ display: inline;
+}
+
+.entry-actions li:not(:last-child):after {
+ content: "|";
+}
+
.entry-meta {
font-size: 0.95em;
margin: 0 0 20px;
diff --git a/server/static/js.go b/server/static/js.go
index a328b05..be2086c 100644
--- a/server/static/js.go
+++ b/server/static/js.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-04 20:40:04.511740583 -0800 PST m=+0.012182340
+// 2017-12-10 18:56:24.37299237 -0800 PST m=+0.014971457
package static
@@ -45,13 +45,16 @@ class EntryHandler{static updateEntriesStatus(entryIDs,status,callback){let url=
static toggleEntryStatus(element){let entryID=parseInt(element.dataset.id,10);let statuses={read:"unread",unread:"read"};for(let currentStatus in statuses){let newStatus=statuses[currentStatus];if(element.classList.contains("item-status-"+currentStatus)){element.classList.remove("item-status-"+currentStatus);element.classList.add("item-status-"+newStatus);this.updateEntriesStatus([entryID],newStatus);break;}}}
static markEntryAsRead(element){if(element.classList.contains("item-status-unread")){element.classList.remove("item-status-unread");element.classList.add("item-status-read");let entryID=parseInt(element.dataset.id,10);this.updateEntriesStatus([entryID],"read");}}
static saveEntry(element){if(element.dataset.completed){return;}
-element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.saveUrl);request.withCallback(()=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;});request.execute();}}
+element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.saveUrl);request.withCallback(()=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;});request.execute();}
+static fetchOriginalContent(element){if(element.dataset.completed){return;}
+element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.fetchContentUrl);request.withCallback((response)=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;response.json().then((data)=>{document.querySelector(".entry-content").innerHTML=data.content;});});request.execute();}}
class ConfirmHandler{remove(url){let request=new RequestBuilder(url);request.withCallback(()=>window.location.reload());request.execute();}
handle(event){let questionElement=document.createElement("span");let linkElement=event.target;let containerElement=linkElement.parentNode;linkElement.style.display="none";let yesElement=document.createElement("a");yesElement.href="#";yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));yesElement.onclick=(event)=>{event.preventDefault();let loadingElement=document.createElement("span");loadingElement.className="loading";loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));questionElement.remove();containerElement.appendChild(loadingElement);this.remove(linkElement.dataset.url);};let noElement=document.createElement("a");noElement.href="#";noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));noElement.onclick=(event)=>{event.preventDefault();linkElement.style.display="inline";questionElement.remove();};questionElement.className="confirm";questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion+" "));questionElement.appendChild(yesElement);questionElement.appendChild(document.createTextNode(", "));questionElement.appendChild(noElement);containerElement.appendChild(questionElement);}}
class MenuHandler{clickMenuListItem(event){let element=event.target;if(element.tagName==="A"){window.location.href=element.getAttribute("href");}else{window.location.href=element.querySelector("a").getAttribute("href");}}
toggleMainMenu(){let menu=document.querySelector(".header nav ul");if(DomHelper.isVisible(menu)){menu.style.display="none";}else{menu.style.display="block";}}}
class NavHandler{markPageAsRead(){let items=DomHelper.getVisibleElements(".items .item");let entryIDs=[];items.forEach((element)=>{element.classList.add("item-status-read");entryIDs.push(parseInt(element.dataset.id,10));});if(entryIDs.length>0){EntryHandler.updateEntriesStatus(entryIDs,"read",()=>{this.goToPage("next",true);});}}
saveEntry(){if(this.isListView()){let currentItem=document.querySelector(".current-item");if(currentItem!==null){let saveLink=currentItem.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}}else{let saveLink=document.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}}
+fetchOriginalContent(){if(!this.isListView()){let link=document.querySelector("a[data-fetch-content-entry]");if(link){EntryHandler.fetchOriginalContent(link);}}}
toggleEntryStatus(){let currentItem=document.querySelector(".current-item");if(currentItem!==null){this.goToNextListItem();EntryHandler.toggleEntryStatus(currentItem);}}
openOriginalLink(){let entryLink=document.querySelector(".entry h1 a");if(entryLink!==null){DomHelper.openNewTab(entryLink.getAttribute("href"));return;}
let currentItemOriginalLink=document.querySelector(".current-item a[data-original-link]");if(currentItemOriginalLink!==null){DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));let currentItem=document.querySelector(".current-item");this.goToNextListItem();EntryHandler.markEntryAsRead(currentItem);}}
@@ -68,9 +71,9 @@ if(currentItem===null){items[0].classList.add("current-item");return;}
for(let i=0;i<items.length;i++){if(items[i].classList.contains("current-item")){items[i].classList.remove("current-item");if(i+1<items.length){items[i+1].classList.add("current-item");DomHelper.scrollPageTo(items[i+1]);}
break;}}}
isListView(){return document.querySelector(".items")!==null;}}
-document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmitButtons();let touchHandler=new TouchHandler();touchHandler.listen();let navHandler=new NavHandler();let keyboardHandler=new KeyboardHandler();keyboardHandler.on("g u",()=>navHandler.goToPage("unread"));keyboardHandler.on("g h",()=>navHandler.goToPage("history"));keyboardHandler.on("g f",()=>navHandler.goToPage("feeds"));keyboardHandler.on("g c",()=>navHandler.goToPage("categories"));keyboardHandler.on("g s",()=>navHandler.goToPage("settings"));keyboardHandler.on("ArrowLeft",()=>navHandler.goToPrevious());keyboardHandler.on("ArrowRight",()=>navHandler.goToNext());keyboardHandler.on("j",()=>navHandler.goToPrevious());keyboardHandler.on("p",()=>navHandler.goToPrevious());keyboardHandler.on("k",()=>navHandler.goToNext());keyboardHandler.on("n",()=>navHandler.goToNext());keyboardHandler.on("h",()=>navHandler.goToPage("previous"));keyboardHandler.on("l",()=>navHandler.goToPage("next"));keyboardHandler.on("o",()=>navHandler.openSelectedItem());keyboardHandler.on("v",()=>navHandler.openOriginalLink());keyboardHandler.on("m",()=>navHandler.toggleEntryStatus());keyboardHandler.on("A",()=>navHandler.markPageAsRead());keyboardHandler.on("s",()=>navHandler.saveEntry());keyboardHandler.listen();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});mouseHandler.onClick("a[data-on-click=markPageAsRead]",()=>navHandler.markPageAsRead());mouseHandler.onClick("a[data-confirm]",(event)=>{(new ConfirmHandler()).handle(event);});if(document.documentElement.clientWidth<600){let menuHandler=new MenuHandler();mouseHandler.onClick(".logo",()=>menuHandler.toggleMainMenu());mouseHandler.onClick(".header nav li",(event)=>menuHandler.clickMenuListItem(event));}});})();`,
+document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmitButtons();let touchHandler=new TouchHandler();touchHandler.listen();let navHandler=new NavHandler();let keyboardHandler=new KeyboardHandler();keyboardHandler.on("g u",()=>navHandler.goToPage("unread"));keyboardHandler.on("g h",()=>navHandler.goToPage("history"));keyboardHandler.on("g f",()=>navHandler.goToPage("feeds"));keyboardHandler.on("g c",()=>navHandler.goToPage("categories"));keyboardHandler.on("g s",()=>navHandler.goToPage("settings"));keyboardHandler.on("ArrowLeft",()=>navHandler.goToPrevious());keyboardHandler.on("ArrowRight",()=>navHandler.goToNext());keyboardHandler.on("j",()=>navHandler.goToPrevious());keyboardHandler.on("p",()=>navHandler.goToPrevious());keyboardHandler.on("k",()=>navHandler.goToNext());keyboardHandler.on("n",()=>navHandler.goToNext());keyboardHandler.on("h",()=>navHandler.goToPage("previous"));keyboardHandler.on("l",()=>navHandler.goToPage("next"));keyboardHandler.on("o",()=>navHandler.openSelectedItem());keyboardHandler.on("v",()=>navHandler.openOriginalLink());keyboardHandler.on("m",()=>navHandler.toggleEntryStatus());keyboardHandler.on("A",()=>navHandler.markPageAsRead());keyboardHandler.on("s",()=>navHandler.saveEntry());keyboardHandler.on("d",()=>navHandler.fetchOriginalContent());keyboardHandler.listen();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});mouseHandler.onClick("a[data-fetch-content-entry]",(event)=>{event.preventDefault();EntryHandler.fetchOriginalContent(event.target);});mouseHandler.onClick("a[data-on-click=markPageAsRead]",()=>navHandler.markPageAsRead());mouseHandler.onClick("a[data-confirm]",(event)=>{(new ConfirmHandler()).handle(event);});if(document.documentElement.clientWidth<600){let menuHandler=new MenuHandler();mouseHandler.onClick(".logo",()=>menuHandler.toggleMainMenu());mouseHandler.onClick(".header nav li",(event)=>menuHandler.clickMenuListItem(event));}});})();`,
}
var JavascriptChecksums = map[string]string{
- "app": "7d14f00cb219662aaf59f20080265097eb236999dcc712b83775882970d05803",
+ "app": "a70092cda52d5c3673e789868d8cfeb73a890e1a931b102a738021b5c2a65519",
}
diff --git a/server/static/js/app.js b/server/static/js/app.js
index 9268536..3305d37 100644
--- a/server/static/js/app.js
+++ b/server/static/js/app.js
@@ -324,6 +324,25 @@ class EntryHandler {
});
request.execute();
}
+
+ static fetchOriginalContent(element) {
+ if (element.dataset.completed) {
+ return;
+ }
+
+ element.innerHTML = element.dataset.labelLoading;
+
+ let request = new RequestBuilder(element.dataset.fetchContentUrl);
+ request.withCallback((response) => {
+ element.innerHTML = element.dataset.labelDone;
+ element.dataset.completed = true;
+
+ response.json().then((data) => {
+ document.querySelector(".entry-content").innerHTML = data.content;
+ });
+ });
+ request.execute();
+ }
}
class ConfirmHandler {
@@ -430,6 +449,15 @@ class NavHandler {
}
}
+ fetchOriginalContent() {
+ if (! this.isListView()){
+ let link = document.querySelector("a[data-fetch-content-entry]");
+ if (link) {
+ EntryHandler.fetchOriginalContent(link);
+ }
+ }
+ }
+
toggleEntryStatus() {
let currentItem = document.querySelector(".current-item");
if (currentItem !== null) {
@@ -577,6 +605,7 @@ document.addEventListener("DOMContentLoaded", function() {
keyboardHandler.on("m", () => navHandler.toggleEntryStatus());
keyboardHandler.on("A", () => navHandler.markPageAsRead());
keyboardHandler.on("s", () => navHandler.saveEntry());
+ keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
keyboardHandler.listen();
let mouseHandler = new MouseHandler();
@@ -584,6 +613,12 @@ document.addEventListener("DOMContentLoaded", function() {
event.preventDefault();
EntryHandler.saveEntry(event.target);
});
+
+ mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
+ event.preventDefault();
+ EntryHandler.fetchOriginalContent(event.target);
+ });
+
mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => navHandler.markPageAsRead());
mouseHandler.onClick("a[data-confirm]", (event) => {
(new ConfirmHandler()).handle(event);
diff --git a/server/template/common.go b/server/template/common.go
index d7afe65..268ae69 100644
--- a/server/template/common.go
+++ b/server/template/common.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-03 17:25:29.427766854 -0800 PST m=+0.040793779
+// 2017-12-10 18:56:24.386027486 -0800 PST m=+0.028006573
package template
diff --git a/server/template/html/entry.html b/server/template/html/entry.html
index 041f1ce..25d7d16 100644
--- a/server/template/html/entry.html
+++ b/server/template/html/entry.html
@@ -7,13 +7,26 @@
<a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
</h1>
<div class="entry-actions">
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
+ <ul>
+ <li>
+ <a href="#"
+ title="{{ t "Save this article" }}"
+ data-save-entry="true"
+ data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Save" }}</a>
+ </li>
+ <li>
+ <a href="#"
+ title="{{ t "Fetch original content" }}"
+ data-fetch-content-entry="true"
+ data-fetch-content-url="{{ route "fetchContent" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Loading..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Fetch original content" }}</a>
+ </li>
+ </ul>
</div>
<div class="entry-meta">
<span class="entry-website">
diff --git a/server/template/views.go b/server/template/views.go
index 80c1286..80d956d 100644
--- a/server/template/views.go
+++ b/server/template/views.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-04 20:56:07.05263963 -0800 PST m=+0.018799946
+// 2017-12-10 18:56:24.375327888 -0800 PST m=+0.017306975
package template
@@ -466,13 +466,26 @@ var templateViewsMap = map[string]string{
<a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
</h1>
<div class="entry-actions">
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
+ <ul>
+ <li>
+ <a href="#"
+ title="{{ t "Save this article" }}"
+ data-save-entry="true"
+ data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Save" }}</a>
+ </li>
+ <li>
+ <a href="#"
+ title="{{ t "Fetch original content" }}"
+ data-fetch-content-entry="true"
+ data-fetch-content-url="{{ route "fetchContent" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Loading..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Fetch original content" }}</a>
+ </li>
+ </ul>
</div>
<div class="entry-meta">
<span class="entry-website">
@@ -1170,7 +1183,7 @@ var templateViewsMapChecksums = map[string]string{
"edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
"edit_feed": "c5bc4c22bf7e8348d880395250545595d21fb8c8e723fc5d7cca68e25d250884",
"edit_user": "82d9749d76ddbd2352816d813c4b1f6d92f2222de678b4afe5821090246735c7",
- "entry": "7b234e551a98233d9797948db8a000e3d10334e17d5b1d5d17552d1406555b34",
+ "entry": "ebcf9bb35812dd02759718f7f7411267e6a6c8efd59a9aa0a0e735bcb88efeff",
"feed_entries": "547c19eb36b20e350ce70ed045173b064cdcd6b114afb241c9f2dda9d88fcc27",
"feeds": "c22af39b42ba9ca69ea0914ca789303ec2c5b484abcd4eaa49016e365381257c",
"history": "9a67599a5d8d67ef958e3f07da339b749f42892667547c9e60a54477e8d32a56",
diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go
index 309fa09..eb47201 100644
--- a/server/ui/controller/entry.go
+++ b/server/ui/controller/entry.go
@@ -8,12 +8,91 @@ import (
"errors"
"log"
+ "github.com/miniflux/miniflux2/integration"
"github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/reader/scraper"
"github.com/miniflux/miniflux2/server/core"
"github.com/miniflux/miniflux2/server/ui/payload"
"github.com/miniflux/miniflux2/storage"
)
+// FetchContent downloads the original HTML page and returns relevant contents.
+func (c *Controller) FetchContent(ctx *core.Context, request *core.Request, response *core.Response) {
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.HTML().BadRequest(err)
+ return
+ }
+
+ user := ctx.LoggedUser()
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
+ if entry == nil {
+ response.JSON().NotFound(errors.New("Entry not found"))
+ return
+ }
+
+ content, err := scraper.Fetch(entry.URL)
+ if err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
+ if len(content) > len(entry.Content) {
+ entry.Content = content
+ c.store.UpdateEntryContent(entry)
+ } else {
+ content = entry.Content
+ }
+
+ response.JSON().Created(map[string]string{"content": content})
+}
+
+// SaveEntry send the link to external services.
+func (c *Controller) SaveEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.HTML().BadRequest(err)
+ return
+ }
+
+ user := ctx.LoggedUser()
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
+ if entry == nil {
+ response.JSON().NotFound(errors.New("Entry not found"))
+ return
+ }
+
+ settings, err := c.store.Integration(user.ID)
+ if err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
+ go func() {
+ integration.SendEntry(entry, settings)
+ }()
+
+ response.JSON().Created(map[string]string{"message": "saved"})
+}
+
// ShowFeedEntry shows a single feed entry in "feed" mode.
func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
diff --git a/server/ui/controller/integrations.go b/server/ui/controller/integrations.go
index ceac09a..2035817 100644
--- a/server/ui/controller/integrations.go
+++ b/server/ui/controller/integrations.go
@@ -6,11 +6,8 @@ package controller
import (
"crypto/md5"
- "errors"
"fmt"
- "github.com/miniflux/miniflux2/integration"
- "github.com/miniflux/miniflux2/model"
"github.com/miniflux/miniflux2/server/core"
"github.com/miniflux/miniflux2/server/ui/form"
)
@@ -73,40 +70,3 @@ func (c *Controller) UpdateIntegration(ctx *core.Context, request *core.Request,
response.Redirect(ctx.Route("integrations"))
}
-
-// SaveEntry send the link to external services.
-func (c *Controller) SaveEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- settings, err := c.store.Integration(user.ID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- go func() {
- integration.SendEntry(entry, settings)
- }()
-
- response.JSON().Created(map[string]string{"message": "saved"})
-}
diff --git a/sql/sql.go b/sql/sql.go
index d3a3897..732fc81 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-03 17:25:29.391052668 -0800 PST m=+0.004079593
+// 2017-12-10 18:56:24.36359961 -0800 PST m=+0.005578697
package sql
diff --git a/storage/entry.go b/storage/entry.go
index 5dfb801..151bf49 100644
--- a/storage/entry.go
+++ b/storage/entry.go
@@ -59,6 +59,23 @@ func (s *Storage) CreateEntry(entry *model.Entry) error {
return nil
}
+// UpdateEntryContent updates entry content.
+func (s *Storage) UpdateEntryContent(entry *model.Entry) error {
+ query := `
+ UPDATE entries SET
+ content=$1
+ WHERE user_id=$2 AND id=$3
+ `
+
+ _, err := s.db.Exec(
+ query,
+ entry.Content,
+ entry.UserID,
+ entry.ID,
+ )
+ return err
+}
+
// UpdateEntry update an entry when a feed is refreshed.
func (s *Storage) UpdateEntry(entry *model.Entry) error {
query := `