0
0
mirror of https://github.com/makew0rld/amfora.git synced 2025-06-30 22:18:12 -04:00

Add pagination

This commit is contained in:
makeworld 2020-12-06 20:57:57 -05:00
parent 1a2fba92c2
commit dd7550dffb
10 changed files with 152 additions and 61 deletions

View File

@ -1,6 +1,6 @@
# Notes # Notes
## Subscriptions (temp) ## Subscriptions
- TODO: remove all logger lines - TODO: remove all logger lines
## Issues ## Issues

View File

@ -130,7 +130,7 @@ Features in *italics* are in the master branch, but not in the latest release.
- https://lists.orbitalfox.eu/archives/gemini/2020/001400.html - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html
- [x] Subscriptions - [x] Subscriptions
- RSS, Atom, and [JSON Feeds](https://jsonfeed.org/) are all supported - RSS, Atom, and [JSON Feeds](https://jsonfeed.org/) are all supported
- So is tracking any page to be notified when it changes - So is tracking a page, to know when its content changes
- [ ] Stream support - [ ] Stream support
- [ ] Table of contents for pages - [ ] Table of contents for pages
- [ ] History browser - [ ] History browser

View File

@ -237,6 +237,7 @@ func Init() error {
viper.SetDefault("subscriptions.popup", true) viper.SetDefault("subscriptions.popup", true)
viper.SetDefault("subscriptions.update_interval", 1800) viper.SetDefault("subscriptions.update_interval", 1800)
viper.SetDefault("subscriptions.workers", 3) viper.SetDefault("subscriptions.workers", 3)
viper.SetDefault("subscriptions.entries_per_page", 20)
viper.SetConfigFile(configPath) viper.SetConfigFile(configPath)
viper.SetConfigType("toml") viper.SetConfigType("toml")

View File

@ -153,6 +153,9 @@ update_interval = 1800 # 30 mins
# update times. Any value below 1 will be corrected to 1. # update times. Any value below 1 will be corrected to 1.
workers = 3 workers = 3
# The number of subscription updates displayed per page.
entries_per_page = 20
[theme] [theme]
# This section is for changing the COLORS used in Amfora. # This section is for changing the COLORS used in Amfora.

View File

@ -150,6 +150,9 @@ update_interval = 1800 # 30 mins
# update times. Any value below 1 will be corrected to 1. # update times. Any value below 1 will be corrected to 1.
workers = 3 workers = 3
# The number of subscription updates displayed per page.
entries_per_page = 20
[theme] [theme]
# This section is for changing the COLORS used in Amfora. # This section is for changing the COLORS used in Amfora.

View File

@ -294,7 +294,7 @@ func Init() {
} }
return nil return nil
case tcell.KeyCtrlA: case tcell.KeyCtrlA:
Subscriptions(tabs[curTab]) Subscriptions(tabs[curTab], "about:subscriptions")
tabs[curTab].addToHistory("about:subscriptions") tabs[curTab].addToHistory("about:subscriptions")
return nil return nil
case tcell.KeyCtrlX: case tcell.KeyCtrlX:
@ -578,8 +578,8 @@ func Reload() {
func URL(u string) { func URL(u string) {
t := tabs[curTab] t := tabs[curTab]
if strings.HasPrefix(u, "about:") { if strings.HasPrefix(u, "about:") {
if ok := handleAbout(t, u); ok { if final, ok := handleAbout(t, u); ok {
t.addToHistory(u) t.addToHistory(final)
} }
return return
} }

View File

@ -169,34 +169,40 @@ func handleFavicon(t *tab, host, old string) {
// //
// It does not add the displayed page to history. // It does not add the displayed page to history.
// //
// It returns a bool indicating if the provided URL could be handled. // It returns the URL displayed, and a bool indicating if the provided
func handleAbout(t *tab, u string) bool { // URL could be handled. The string returned will always be empty
// if the bool is false.
func handleAbout(t *tab, u string) (string, bool) {
if !strings.HasPrefix(u, "about:") { if !strings.HasPrefix(u, "about:") {
return false return "", false
} }
switch u { switch u {
case "about:bookmarks": case "about:bookmarks":
Bookmarks(t) Bookmarks(t)
return true return u, true
case "about:subscriptions":
Subscriptions(t)
return true
case "about:newtab": case "about:newtab":
temp := newTabPage // Copy temp := newTabPage // Copy
setPage(t, &temp) setPage(t, &temp)
t.applyBottomBar() t.applyBottomBar()
return true return u, true
} }
if u == "about:subscriptions" || (len(u) > 20 && u[:20] == "about:subscriptions?") {
// about:subscriptions?2 views page 2
return Subscriptions(t, u), true
}
if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") { if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") {
ManageSubscriptions(t, u) ManageSubscriptions(t, u)
// Don't count remove command in history // Don't count remove command in history
return u == "about:manage-subscriptions" if u == "about:manage-subscriptions" {
return u, true
}
return "", false
} }
Error("Error", "Not a valid 'about:' URL.") Error("Error", "Not a valid 'about:' URL.")
return false return "", false
} }
// handleURL displays whatever action is needed for the provided URL, // handleURL displays whatever action is needed for the provided URL,
@ -252,7 +258,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
App.SetFocus(t.view) App.SetFocus(t.view)
if strings.HasPrefix(u, "about:") { if strings.HasPrefix(u, "about:") {
return ret(u, handleAbout(t, u)) return ret(handleAbout(t, u))
} }
u = normalizeURL(u) u = normalizeURL(u)

View File

@ -20,8 +20,8 @@ import (
// It will handle setting the bottomBar. // It will handle setting the bottomBar.
func followLink(t *tab, prev, next string) { func followLink(t *tab, prev, next string) {
if strings.HasPrefix(next, "about:") { if strings.HasPrefix(next, "about:") {
if ok := handleAbout(t, next); ok { if final, ok := handleAbout(t, next); ok {
t.addToHistory(next) t.addToHistory(final)
} }
return return
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"path" "path"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,7 +21,9 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var subscriptionPageUpdated time.Time // Map page number (zero-indexed) to the time it was made at.
// This allows for caching the pages until there's an update.
var subscriptionPageUpdated = make(map[int]time.Time)
// toLocalDay truncates the provided time to a date only, // toLocalDay truncates the provided time to a date only,
// but converts to the local time first. // but converts to the local time first.
@ -30,24 +33,79 @@ func toLocalDay(t time.Time) time.Time {
} }
// Subscriptions displays the subscriptions page on the current tab. // Subscriptions displays the subscriptions page on the current tab.
func Subscriptions(t *tab) { func Subscriptions(t *tab, u string) string {
logger.Log.Println("display.Subscriptions called") logger.Log.Println("display.Subscriptions called")
pageN := 0 // Pages are zero-indexed internally
// Correct URL if query string exists
// The only valid query string is an int above 1.
// Anything "redirects" to the first page, with no query string.
// This is done over just serving the first page content for
// invalid query strings so that there won't be duplicate caches.
correctURL := func(u2 string) string {
if len(u2) > 20 && u2[:20] == "about:subscriptions?" {
query, err := gemini.QueryUnescape(u2[20:])
if err != nil {
return "about:subscriptions"
}
// Valid query string
i, err := strconv.Atoi(query)
if err != nil {
// Not an int
return "about:subscriptions"
}
if i < 2 {
return "about:subscriptions"
}
// Valid int above 1
pageN = i - 1 // Pages are zero-indexed internally
return u2
}
return u2
}
u = correctURL(u)
// Retrieve cached version if there hasn't been any updates // Retrieve cached version if there hasn't been any updates
p, ok := cache.GetPage("about:subscriptions") p, ok := cache.GetPage(u)
if subscriptionPageUpdated.After(subscriptions.LastUpdated) && ok { if subscriptionPageUpdated[pageN].After(subscriptions.LastUpdated) && ok {
logger.Log.Println("using cached subscriptions page") logger.Log.Println("using cached subscriptions page")
setPage(t, p) setPage(t, p)
t.applyBottomBar() t.applyBottomBar()
return return u
} }
logger.Log.Println("started rendering subscriptions page") logger.Log.Println("started rendering subscriptions page")
rawPage := "# Subscriptions\n\n" + pe := subscriptions.GetPageEntries()
"See the help (by pressing ?) for details on how to use this page.\n\n" +
"If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n\n" + // Figure out where the entries for this page start, if at all.
"=> about:manage-subscriptions Manage subscriptions\n" epp := viper.GetInt("subscriptions.entries_per_page")
if epp <= 0 {
epp = 1
}
start := pageN * epp // Index of the first page entry to be displayed
end := start + epp
if end > len(pe.Entries) {
end = len(pe.Entries)
}
var rawPage string
if pageN == 0 {
rawPage = "# Subscriptions\n\n" + rawPage
} else {
rawPage = fmt.Sprintf("# Subscriptions (page %d)\n\n", pageN+1) + rawPage
}
if start > len(pe.Entries)-1 && len(pe.Entries) != 0 {
// The page is out of range, doesn't exist
rawPage += "This page does not exist.\n\n=> about:subscriptions Subscriptions\n"
} else {
// Render page
rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed. See the online wiki for more.\n" +
"If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n" +
"=> about:manage-subscriptions Manage subscriptions\n\n"
// curDay represents what day of posts the loop is on. // curDay represents what day of posts the loop is on.
// It only goes backwards in time. // It only goes backwards in time.
@ -57,12 +115,12 @@ func Subscriptions(t *tab) {
// 26 hours was chosen because it is the largest timezone difference // 26 hours was chosen because it is the largest timezone difference
// currently in the world. Posts may be dated in the future // currently in the world. Posts may be dated in the future
// due to software bugs, where the local user's date is used, but // due to software bugs, where the local user's date is used, but
// the UTC timezone is specified. I believe gemfeed does this. // the UTC timezone is specified. Gemfeed does this at the time of
// writing, but will not after #3 gets merged on its repo. Still,
// the older version will be used for a while.
curDay := toLocalDay(time.Now()).Add(26 * time.Hour) curDay := toLocalDay(time.Now()).Add(26 * time.Hour)
pe := subscriptions.GetPageEntries() for _, entry := range pe.Entries[start:end] { // From new to old
for _, entry := range pe.Entries { // From new to old
// Convert to local time, remove sub-day info // Convert to local time, remove sub-day info
pub := toLocalDay(entry.Published) pub := toLocalDay(entry.Published)
@ -81,12 +139,28 @@ func Subscriptions(t *tab) {
} }
} }
if pageN == 0 && len(pe.Entries) > epp {
// First page, and there's more than can fit
rawPage += "\n\n=> about:subscriptions?2 Next Page\n"
} else if pageN > 0 {
// A later page
rawPage += fmt.Sprintf(
"\n\n=> about:subscriptions?%d Previous Page\n",
pageN, // pageN is zero-indexed but the query string is one-indexed
)
if end != len(pe.Entries)-1 {
// There's more
rawPage += fmt.Sprintf("=> about:subscriptions?%d Next Page\n", pageN+2)
}
}
}
content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false) content, links := renderer.RenderGemini(rawPage, textWidth(), leftMargin(), false)
page := structs.Page{ page := structs.Page{
Raw: rawPage, Raw: rawPage,
Content: content, Content: content,
Links: links, Links: links,
URL: "about:subscriptions", URL: u,
Width: termW, Width: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
@ -94,9 +168,11 @@ func Subscriptions(t *tab) {
setPage(t, &page) setPage(t, &page)
t.applyBottomBar() t.applyBottomBar()
subscriptionPageUpdated = time.Now() subscriptionPageUpdated[pageN] = time.Now()
logger.Log.Println("done rendering subscriptions page") logger.Log.Println("done rendering subscriptions page")
return u
} }
// ManageSubscriptions displays the subscription managing page in // ManageSubscriptions displays the subscription managing page in
@ -109,9 +185,13 @@ func ManageSubscriptions(t *tab, u string) {
} }
rawPage := "# Manage Subscriptions\n\n" + rawPage := "# Manage Subscriptions\n\n" +
"Below is list of URLs, both feeds and pages. Navigate to the link to unsubscribe from that feed or page.\n\n" "Below is list of URLs you are subscribed to, both feeds and pages. " +
"Navigate to the link to unsubscribe from that feed or page.\n\n"
for _, u2 := range subscriptions.AllURLS() { urls := subscriptions.AllURLS()
sort.Strings(urls)
for _, u2 := range urls {
rawPage += fmt.Sprintf( rawPage += fmt.Sprintf(
"=>%s %s\n", "=>%s %s\n",
"about:manage-subscriptions?"+gemini.QueryEscape(u2), "about:manage-subscriptions?"+gemini.QueryEscape(u2),

View File

@ -11,7 +11,6 @@ import (
"os" "os"
"path" "path"
"reflect" "reflect"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -370,7 +369,7 @@ func updateAll() {
wg.Wait() wg.Wait()
} }
// AllURLs returns all the subscribed-to URLS, sorted alphabetically. // AllURLs returns all the subscribed-to URLS.
func AllURLS() []string { func AllURLS() []string {
data.RLock() data.RLock()
defer data.RUnlock() defer data.RUnlock()
@ -386,7 +385,6 @@ func AllURLS() []string {
i++ i++
} }
sort.Strings(urls)
return urls return urls
} }
@ -394,7 +392,7 @@ func AllURLS() []string {
// The URL must be provided. It will do nothing if the URL is // The URL must be provided. It will do nothing if the URL is
// not an actual subscription. // not an actual subscription.
// //
// It returns any errors that occured when saving to disk. // It returns any errors that occurred when saving to disk.
func Remove(u string) error { func Remove(u string) error {
data.Lock() data.Lock()
// Just delete from both instead of using a loop to find it // Just delete from both instead of using a loop to find it