mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-09-11 14:55:51 +03:00
🚧 Adding & displaying feeds/pages works
See NOTES.md for things still left
This commit is contained in:
parent
781b89af61
commit
cf5e65f75a
8
NOTES.md
8
NOTES.md
@ -1,5 +1,13 @@
|
||||
# Notes
|
||||
|
||||
## Temp
|
||||
- Recalculating `about:feeds` adds pages multiple times to the view
|
||||
- Only options for feed files is the download modal - there should be a feed modal before that one
|
||||
- Auto feed detection fails on `ebc.li/atom.xml`
|
||||
|
||||
- TODO: remove all logger lines
|
||||
|
||||
|
||||
## Issues
|
||||
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
|
||||
|
||||
|
11
amfora.go
11
amfora.go
@ -7,6 +7,7 @@ import (
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/display"
|
||||
"github.com/makeworld-the-better-one/amfora/feeds"
|
||||
"github.com/makeworld-the-better-one/amfora/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -16,10 +17,10 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// err := logger.Init()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
err := logger.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
if os.Args[1] == "--version" || os.Args[1] == "-v" {
|
||||
@ -38,7 +39,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
err := config.Init()
|
||||
err = config.Init()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -6,7 +6,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@ -41,7 +40,6 @@ var bkmkPath string
|
||||
var DownloadsDir string
|
||||
|
||||
// Feeds
|
||||
var FeedJSON io.ReadCloser
|
||||
var feedDir string
|
||||
var FeedPath string
|
||||
|
||||
@ -158,8 +156,6 @@ func Init() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, _ = os.OpenFile(FeedPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
FeedJSON = f
|
||||
|
||||
// *** Downloads paths, setup, and creation ***
|
||||
|
||||
@ -234,6 +230,7 @@ func Init() error {
|
||||
viper.SetDefault("a-general.page_max_size", 2097152)
|
||||
viper.SetDefault("a-general.page_max_time", 10)
|
||||
viper.SetDefault("a-general.emoji_favicons", false)
|
||||
viper.SetDefault("a-general.feeds_popup", true)
|
||||
viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
|
||||
viper.SetDefault("url-handlers.other", "off")
|
||||
viper.SetDefault("cache.max_size", 0)
|
||||
|
@ -68,6 +68,9 @@ page_max_time = 10
|
||||
# Whether to replace tab numbers with emoji favicons, which are cached.
|
||||
emoji_favicons = false
|
||||
|
||||
# Whether a pop-up appears when viewing a potential feed
|
||||
feed_popup = true
|
||||
|
||||
|
||||
[auth]
|
||||
# Authentication settings
|
||||
@ -189,6 +192,8 @@ max_pages = 30 # The maximum number of pages the cache will store
|
||||
# yesno_modal_text
|
||||
# tofu_modal_bg
|
||||
# tofu_modal_text
|
||||
# feed_modal_bg
|
||||
# feed_modal_text
|
||||
|
||||
# input_modal_bg
|
||||
# input_modal_text
|
||||
|
@ -38,6 +38,8 @@ var theme = map[string]tcell.Color{
|
||||
"yesno_modal_text": tcell.ColorWhite,
|
||||
"tofu_modal_bg": tcell.ColorMaroon,
|
||||
"tofu_modal_text": tcell.ColorWhite,
|
||||
"feed_modal_bg": tcell.Color61, // xterm:SlateBlue3, #5f5faf
|
||||
"feed_modal_text": tcell.ColorWhite,
|
||||
|
||||
"input_modal_bg": tcell.ColorGreen,
|
||||
"input_modal_text": tcell.ColorWhite,
|
||||
|
@ -65,6 +65,9 @@ page_max_time = 10
|
||||
# Whether to replace tab numbers with emoji favicons, which are cached.
|
||||
emoji_favicons = false
|
||||
|
||||
# Whether a pop-up appears when viewing a potential feed
|
||||
feed_popup = true
|
||||
|
||||
|
||||
[auth]
|
||||
# Authentication settings
|
||||
@ -186,6 +189,8 @@ max_pages = 30 # The maximum number of pages the cache will store
|
||||
# yesno_modal_text
|
||||
# tofu_modal_bg
|
||||
# tofu_modal_text
|
||||
# feed_modal_bg
|
||||
# feed_modal_text
|
||||
|
||||
# input_modal_bg
|
||||
# input_modal_text
|
||||
|
@ -207,6 +207,7 @@ func Init() {
|
||||
})
|
||||
|
||||
// Render the default new tab content ONCE and store it for later
|
||||
// This code is repeated in Reload()
|
||||
newTabContent := getNewTabContent()
|
||||
renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), leftMargin(), false)
|
||||
newTabPage = structs.Page{
|
||||
@ -292,6 +293,13 @@ func Init() {
|
||||
Info("The current page has no content, so it couldn't be downloaded.")
|
||||
}
|
||||
return nil
|
||||
case tcell.KeyCtrlA:
|
||||
Feeds(tabs[curTab])
|
||||
tabs[curTab].addToHistory("about:feeds")
|
||||
return nil
|
||||
case tcell.KeyCtrlX:
|
||||
go addFeed()
|
||||
return nil
|
||||
case tcell.KeyRune:
|
||||
// Regular key was sent
|
||||
switch string(event.Rune()) {
|
||||
@ -575,6 +583,11 @@ func URL(u string) {
|
||||
tabs[curTab].addToHistory("about:bookmarks")
|
||||
return
|
||||
}
|
||||
if u == "about:feeds" { //nolint:goconst
|
||||
Feeds(tabs[curTab])
|
||||
tabs[curTab].addToHistory("about:feeds")
|
||||
return
|
||||
}
|
||||
if u == "about:newtab" {
|
||||
temp := newTabPage // Copy
|
||||
setPage(tabs[curTab], &temp)
|
||||
|
118
display/feeds.go
118
display/feeds.go
@ -2,17 +2,25 @@ package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/feeds"
|
||||
"github.com/makeworld-the-better-one/amfora/logger"
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var feedPageRaw = "# Feeds & Pages\n\nUpdates" + strings.Repeat(" ", 80-25) + "[Newest -> Oldest]\n" +
|
||||
strings.Repeat("-", 80) + "\n\n"
|
||||
strings.Repeat("-", 80) + "\nSee the help (by pressing ?) for details on how to use this page.\n\n"
|
||||
|
||||
var feedPageUpdated time.Time
|
||||
|
||||
@ -25,6 +33,8 @@ func toLocalDay(t time.Time) time.Time {
|
||||
|
||||
// Feeds displays the feeds page on the current tab.
|
||||
func Feeds(t *tab) {
|
||||
logger.Log.Println("display.Feeds called")
|
||||
|
||||
// Retrieve cached version if there hasn't been any updates
|
||||
p, ok := cache.GetPage("about:feeds")
|
||||
if feedPageUpdated.After(feeds.LastUpdated) && ok {
|
||||
@ -74,6 +84,108 @@ func Feeds(t *tab) {
|
||||
feedPageUpdated = time.Now()
|
||||
}
|
||||
|
||||
func feedInit() {
|
||||
// TODO
|
||||
// openFeedModal displays the "Add feed/page" modal
|
||||
// It returns whether the user wanted to add the feed/page.
|
||||
// The tracked arg specifies whether this feed/page is already
|
||||
// being tracked.
|
||||
func openFeedModal(validFeed, tracked bool) bool {
|
||||
logger.Log.Println("display.openFeedModal called")
|
||||
// Reuses yesNoModal
|
||||
|
||||
if viper.GetBool("a-general.color") {
|
||||
yesNoModal.
|
||||
SetBackgroundColor(config.GetColor("feed_modal_bg")).
|
||||
SetTextColor(config.GetColor("feed_modal_text"))
|
||||
yesNoModal.GetFrame().
|
||||
SetBorderColor(config.GetColor("feed_modal_text")).
|
||||
SetTitleColor(config.GetColor("feed_modal_text"))
|
||||
} else {
|
||||
yesNoModal.
|
||||
SetBackgroundColor(tcell.ColorBlack).
|
||||
SetTextColor(tcell.ColorWhite)
|
||||
yesNoModal.GetFrame().
|
||||
SetBorderColor(tcell.ColorWhite).
|
||||
SetTitleColor(tcell.ColorWhite)
|
||||
}
|
||||
if validFeed {
|
||||
yesNoModal.GetFrame().SetTitle("Feed Tracking")
|
||||
if tracked {
|
||||
yesNoModal.SetText("This is already being tracked. Would you like to manually update it?")
|
||||
} else {
|
||||
yesNoModal.SetText("Would you like to start tracking this feed?")
|
||||
}
|
||||
} else {
|
||||
yesNoModal.GetFrame().SetTitle("Page Tracking")
|
||||
if tracked {
|
||||
yesNoModal.SetText("This is already being tracked. Would you like to manually update it?")
|
||||
} else {
|
||||
yesNoModal.SetText("Would you like to start tracking this page?")
|
||||
}
|
||||
}
|
||||
|
||||
tabPages.ShowPage("yesno")
|
||||
tabPages.SendToFront("yesno")
|
||||
App.SetFocus(yesNoModal)
|
||||
App.Draw()
|
||||
|
||||
resp := <-yesNoCh
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
App.SetFocus(tabs[curTab].view)
|
||||
App.Draw()
|
||||
return resp
|
||||
}
|
||||
|
||||
// getFeedFromPage is like feeds.GetFeed but takes a structs.Page as input.
|
||||
func getFeedFromPage(p *structs.Page) (*gofeed.Feed, bool) {
|
||||
parsed, _ := url.Parse(p.URL)
|
||||
filename := path.Base(parsed.Path)
|
||||
r := strings.NewReader(p.Raw)
|
||||
return feeds.GetFeed(p.RawMediatype, filename, r)
|
||||
}
|
||||
|
||||
// addFeedDirect is only for adding feeds, not pages.
|
||||
// It's for when you already have a feed and know if it's tracked.
|
||||
// Use mainly by handleURL because it already did a lot of the work.
|
||||
//
|
||||
// Like addFeed, it should be called in a goroutine.
|
||||
func addFeedDirect(u string, feed *gofeed.Feed, tracked bool) {
|
||||
logger.Log.Println("display.addFeedDirect called")
|
||||
|
||||
if openFeedModal(true, tracked) {
|
||||
err := feeds.AddFeed(u, feed)
|
||||
if err != nil {
|
||||
Error("Feed Error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addFeed goes through the process of adding a bookmark for the current page.
|
||||
// It is the high-level way of doing it. It should be called in a goroutine.
|
||||
func addFeed() {
|
||||
logger.Log.Println("display.addFeed called")
|
||||
|
||||
t := tabs[curTab]
|
||||
p := t.page
|
||||
|
||||
if !t.hasContent() {
|
||||
// It's an about: page, or a malformed one
|
||||
return
|
||||
}
|
||||
|
||||
feed, isFeed := getFeedFromPage(p)
|
||||
tracked := feeds.IsTracked(p.URL)
|
||||
|
||||
if openFeedModal(isFeed, tracked) {
|
||||
var err error
|
||||
|
||||
if isFeed {
|
||||
err = feeds.AddFeed(p.URL, feed)
|
||||
} else {
|
||||
err = feeds.AddPage(p.URL, strings.NewReader(p.Raw))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error("Feed/Page Error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ Ctrl-R, R|Reload a page, discarding the cached version.
|
||||
Ctrl-B|View bookmarks
|
||||
Ctrl-D|Add, change, or remove a bookmark for the current page.
|
||||
Ctrl-S|Save the current page to your downloads.
|
||||
Ctrl-A|View tracked feeds and pages.
|
||||
Ctrl-X|Track or update the current feed/page.
|
||||
q, Ctrl-Q|Quit
|
||||
Ctrl-C|Hard quit. This can be used when in the middle of downloading,
|
||||
|for example.
|
||||
|
@ -150,7 +150,6 @@ func modalInit() {
|
||||
|
||||
bkmkInit()
|
||||
dlInit()
|
||||
feedInit()
|
||||
}
|
||||
|
||||
// Error displays an error on the screen in a modal.
|
||||
|
@ -18,6 +18,7 @@ You can customize this page by creating a gemtext file called newtab.gmi, in Amf
|
||||
Happy browsing!
|
||||
|
||||
=> about:bookmarks Bookmarks
|
||||
=> about:feeds Feed and Page Tracking
|
||||
|
||||
=> //gemini.circumlunar.space Project Gemini
|
||||
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
"github.com/makeworld-the-better-one/amfora/client"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/feeds"
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/webbrowser"
|
||||
@ -36,6 +37,11 @@ func followLink(t *tab, prev, next string) {
|
||||
t.addToHistory("about:bookmarks")
|
||||
return
|
||||
}
|
||||
if next == "about:feeds" {
|
||||
Feeds(t)
|
||||
t.addToHistory("about:feeds")
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(next, "about:") {
|
||||
Error("Error", "Not a valid 'about:' URL for linking")
|
||||
return
|
||||
@ -328,6 +334,20 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
t.barText = oldText
|
||||
}
|
||||
t.mode = tabModeDone
|
||||
|
||||
go func(p *structs.Page) {
|
||||
if b && t.hasContent() && !feeds.IsTracked(s) && viper.GetBool("a-general.feed_popup") {
|
||||
// The current page might be an untracked feed, and the user wants
|
||||
// to be notified in such cases.
|
||||
|
||||
feed, isFeed := getFeedFromPage(p)
|
||||
if isFeed && isValidTab(t) && t.page == p {
|
||||
// After parsing and track-checking time, the page is still being displayed
|
||||
addFeedDirect(p.URL, feed, false)
|
||||
}
|
||||
}
|
||||
}(t.page)
|
||||
|
||||
return s, b
|
||||
}
|
||||
|
||||
@ -341,6 +361,10 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
Bookmarks(t)
|
||||
return ret("about:bookmarks", true)
|
||||
}
|
||||
if u == "about:feeds" {
|
||||
Feeds(t)
|
||||
return ret("about:feeds", true)
|
||||
}
|
||||
|
||||
u = normalizeURL(u)
|
||||
u = cache.Redirect(u)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
urlPkg "net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/client"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/logger"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/mmcdole/gofeed"
|
||||
)
|
||||
@ -38,22 +40,32 @@ var LastUpdated time.Time
|
||||
|
||||
// Init should be called after config.Init.
|
||||
func Init() error {
|
||||
defer config.FeedJSON.Close()
|
||||
f, err := os.Open(config.FeedPath)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
|
||||
dec := json.NewDecoder(config.FeedJSON)
|
||||
err := dec.Decode(&data)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("feeds json is corrupted: %v", err) //nolint:goerr113
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Size() > 0 {
|
||||
dec := json.NewDecoder(f)
|
||||
err = dec.Decode(&data)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("feeds.json is corrupted: %w", err) //nolint:goerr113
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
// There's an error opening the file, but it's not bc is doesn't exist
|
||||
return fmt.Errorf("open feeds.json error: %w", err) //nolint:goerr113
|
||||
}
|
||||
|
||||
LastUpdated = time.Now()
|
||||
|
||||
go updateAll()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTracked returns true if the feed/page URL is already being tracked.
|
||||
func IsTracked(url string) bool {
|
||||
logger.Log.Println("feeds.IsTracked called")
|
||||
|
||||
data.feedMu.RLock()
|
||||
for u := range data.Feeds {
|
||||
if url == u {
|
||||
@ -76,6 +88,8 @@ func IsTracked(url string) bool {
|
||||
// GetFeed returns a Feed object and a bool indicating whether the passed
|
||||
// content was actually recognized as a feed.
|
||||
func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) {
|
||||
logger.Log.Println("feeds.GetFeed called")
|
||||
|
||||
if r == nil {
|
||||
return nil, false
|
||||
}
|
||||
@ -95,11 +109,14 @@ func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) {
|
||||
}
|
||||
|
||||
func writeJSON() error {
|
||||
logger.Log.Println("feeds.writeJSON called")
|
||||
|
||||
writeMu.Lock()
|
||||
defer writeMu.Unlock()
|
||||
|
||||
f, err := os.OpenFile(config.FeedPath, os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
logger.Log.Println("feeds.writeJSON error", err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
@ -108,9 +125,14 @@ func writeJSON() error {
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
data.Lock()
|
||||
logger.Log.Println("feeds.writeJSON acquired data lock")
|
||||
err = enc.Encode(&data)
|
||||
data.Unlock()
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Println("feeds.writeJSON error", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -118,6 +140,8 @@ func writeJSON() error {
|
||||
// It can be used to update a feed for a URL, although the package
|
||||
// will handle that on its own.
|
||||
func AddFeed(url string, feed *gofeed.Feed) error {
|
||||
logger.Log.Println("feeds.AddFeed called")
|
||||
|
||||
if feed == nil {
|
||||
panic("feed is nil")
|
||||
}
|
||||
@ -128,17 +152,20 @@ func AddFeed(url string, feed *gofeed.Feed) error {
|
||||
}
|
||||
|
||||
data.feedMu.Lock()
|
||||
data.Feeds[url] = feed
|
||||
err := writeJSON()
|
||||
if err != nil {
|
||||
// Don't use in-memory if it couldn't be saved
|
||||
delete(data.Feeds, url)
|
||||
data.feedMu.Unlock()
|
||||
return ErrSaving
|
||||
}
|
||||
data.feedMu.Unlock()
|
||||
oldFeed, ok := data.Feeds[url]
|
||||
if !ok || !reflect.DeepEqual(feed, oldFeed) {
|
||||
// Feeds are different, or there was never an old one
|
||||
|
||||
LastUpdated = time.Now()
|
||||
data.Feeds[url] = feed
|
||||
data.feedMu.Unlock()
|
||||
err := writeJSON()
|
||||
if err != nil {
|
||||
return ErrSaving
|
||||
}
|
||||
LastUpdated = time.Now()
|
||||
} else {
|
||||
data.feedMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -146,6 +173,8 @@ func AddFeed(url string, feed *gofeed.Feed) error {
|
||||
// It can be used to update the page as well, although the package
|
||||
// will handle that on its own.
|
||||
func AddPage(url string, r io.Reader) error {
|
||||
logger.Log.Println("feeds.AddPage called")
|
||||
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
@ -164,22 +193,23 @@ func AddPage(url string, r io.Reader) error {
|
||||
Hash: newHash,
|
||||
Changed: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
err := writeJSON()
|
||||
if err != nil {
|
||||
// Don't use in-memory if it couldn't be saved
|
||||
delete(data.Pages, url)
|
||||
data.pageMu.Unlock()
|
||||
return err
|
||||
err := writeJSON()
|
||||
if err != nil {
|
||||
return ErrSaving
|
||||
}
|
||||
LastUpdated = time.Now()
|
||||
} else {
|
||||
data.pageMu.Unlock()
|
||||
}
|
||||
data.pageMu.Unlock()
|
||||
|
||||
LastUpdated = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateFeed(url string) error {
|
||||
logger.Log.Println("feeds.updateFeed called")
|
||||
|
||||
res, err := client.Fetch(url)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
@ -205,6 +235,8 @@ func updateFeed(url string) error {
|
||||
}
|
||||
|
||||
func updatePage(url string) error {
|
||||
logger.Log.Println("feeds.updatePage called")
|
||||
|
||||
res, err := client.Fetch(url)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
@ -224,6 +256,8 @@ func updatePage(url string) error {
|
||||
// updateAll updates all feeds and pages using workers.
|
||||
// It only returns once all the workers are done.
|
||||
func updateAll() {
|
||||
logger.Log.Println("feeds.updateAll called")
|
||||
|
||||
// TODO: Is two goroutines the right amount?
|
||||
|
||||
worker := func(jobs <-chan [2]string, wg *sync.WaitGroup) {
|
||||
@ -246,10 +280,19 @@ func updateAll() {
|
||||
numJobs := len(data.Feeds) + len(data.Pages)
|
||||
jobs := make(chan [2]string, numJobs)
|
||||
|
||||
if numJobs == 0 {
|
||||
data.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Start 2 workers, waiting for jobs
|
||||
for w := 0; w < 2; w++ {
|
||||
wg.Add(1)
|
||||
go worker(jobs, &wg)
|
||||
go func(i int) {
|
||||
logger.Log.Println("started worker", i)
|
||||
worker(jobs, &wg)
|
||||
logger.Log.Println("ended worker", i)
|
||||
}(w)
|
||||
}
|
||||
|
||||
// Get map keys in a slice
|
||||
@ -277,6 +320,7 @@ func updateAll() {
|
||||
jobs <- [2]string{"page", pageKeys[j-len(feedKeys)]}
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@ -287,6 +331,8 @@ func updateAll() {
|
||||
// so this function needs to be called again to get updates.
|
||||
// It always returns sorted entries - by post time, from newest to oldest.
|
||||
func GetPageEntries() *PageEntries {
|
||||
logger.Log.Println("feeds.GetPageEntries called")
|
||||
|
||||
var pe PageEntries
|
||||
|
||||
data.RLock()
|
||||
|
@ -34,8 +34,8 @@ The time is in RFC 3339 format, preferably in the UTC timezone.
|
||||
|
||||
// Decoded JSON
|
||||
type jsonData struct {
|
||||
feedMu sync.RWMutex
|
||||
pageMu sync.RWMutex
|
||||
feedMu *sync.RWMutex
|
||||
pageMu *sync.RWMutex
|
||||
Feeds map[string]*gofeed.Feed `json:"feeds,omitempty"`
|
||||
Pages map[string]*pageJSON `json:"pages,omitempty"`
|
||||
}
|
||||
@ -69,7 +69,13 @@ type pageJSON struct {
|
||||
Changed time.Time `json:"changed"` // When the latest change happened
|
||||
}
|
||||
|
||||
var data jsonData // Global instance of jsonData - loaded from JSON and used
|
||||
// Global instance of jsonData - loaded from JSON and used
|
||||
var data = jsonData{
|
||||
feedMu: &sync.RWMutex{},
|
||||
pageMu: &sync.RWMutex{},
|
||||
Feeds: make(map[string]*gofeed.Feed),
|
||||
Pages: make(map[string]*pageJSON),
|
||||
}
|
||||
|
||||
// PageEntry is a single item on a feed page.
|
||||
// It is used both for tracked feeds and pages.
|
||||
|
@ -104,31 +104,34 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
|
||||
if mediatype == "text/gemini" {
|
||||
rendered, links := RenderGemini(utfText, width, leftMargin, proxied)
|
||||
return &structs.Page{
|
||||
Mediatype: structs.TextGemini,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: rendered,
|
||||
Links: links,
|
||||
Mediatype: structs.TextGemini,
|
||||
RawMediatype: mediatype,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: rendered,
|
||||
Links: links,
|
||||
}, nil
|
||||
} else if strings.HasPrefix(mediatype, "text/") {
|
||||
if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") {
|
||||
// ANSI
|
||||
return &structs.Page{
|
||||
Mediatype: structs.TextAnsi,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: RenderANSI(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
Mediatype: structs.TextAnsi,
|
||||
RawMediatype: mediatype,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: RenderANSI(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Treated as plaintext
|
||||
return &structs.Page{
|
||||
Mediatype: structs.TextPlain,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: RenderPlainText(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
Mediatype: structs.TextPlain,
|
||||
RawMediatype: mediatype,
|
||||
URL: url,
|
||||
Raw: utfText,
|
||||
Content: RenderPlainText(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -18,18 +18,19 @@ const (
|
||||
|
||||
// Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
|
||||
type Page struct {
|
||||
URL string
|
||||
Mediatype Mediatype
|
||||
Raw string // The raw response, as received over the network
|
||||
Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
|
||||
Links []string // URLs, for each region in the content.
|
||||
Row int // Scroll position
|
||||
Column int // ditto
|
||||
Width int // The terminal width when the Content was set, to know when reformatting should happen.
|
||||
Selected string // The current text or link selected
|
||||
SelectedID string // The cview region ID for the selected text/link
|
||||
Mode PageMode
|
||||
Favicon string
|
||||
URL string
|
||||
Mediatype Mediatype // Used for rendering purposes, generalized
|
||||
RawMediatype string // The actual mediatype sent by the server
|
||||
Raw string // The raw response, as received over the network
|
||||
Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
|
||||
Links []string // URLs, for each region in the content.
|
||||
Row int // Scroll position
|
||||
Column int // ditto
|
||||
Width int // The terminal width when the Content was set, to know when reformatting should happen.
|
||||
Selected string // The current text or link selected
|
||||
SelectedID string // The cview region ID for the selected text/link
|
||||
Mode PageMode
|
||||
Favicon string
|
||||
}
|
||||
|
||||
// Size returns an approx. size of a Page in bytes.
|
||||
|
Loading…
Reference in New Issue
Block a user