mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-09-11 14:55:51 +03:00
🚧 Downloading all content seems to work
This commit is contained in:
parent
39fa7c6a8b
commit
eae118faac
@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- **Downloading pages and any content** (#38)
|
||||
- Link and heading lines are wrapped just like regular text lines
|
||||
- Wrapped list items are indented to stay behind the bullet (#35)
|
||||
- Certificate expiry date is stored when the cert IDs match (#39)
|
||||
|
4
NOTES.md
4
NOTES.md
@ -1,9 +1,9 @@
|
||||
# Notes
|
||||
|
||||
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
|
||||
|
||||
## Issues
|
||||
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
|
||||
- Can't go back or do other things while page is loading - need a way to stop `handleURL`
|
||||
- dlChoiceModal doesn't go away when portal is selected, and freezes on Cancel
|
||||
|
||||
## Upstream Bugs
|
||||
- Wrapping messes up on brackets
|
||||
|
15
README.md
15
README.md
@ -35,6 +35,19 @@ curl -sSL https://raw.githubusercontent.com/makeworld-the-better-one/amfora/mast
|
||||
update-desktop-database ~/.local/share/applications
|
||||
```
|
||||
|
||||
### For developers
|
||||
This section is for programmers who want to install from source.
|
||||
|
||||
Install latest release:
|
||||
```
|
||||
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora
|
||||
```
|
||||
|
||||
Install latest commit:
|
||||
```
|
||||
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora@master
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Just call `amfora` or `amfora <url>` on the terminal. On Windows it might be `amfora.exe` instead.
|
||||
@ -59,8 +72,8 @@ Features in *italics* are in the master branch, but not in the latest release.
|
||||
- [x] Multiple charset support (over 55)
|
||||
- [x] Built-in search (uses GUS by default)
|
||||
- [x] Bookmarks
|
||||
- [x] *Download pages and arbitrary data*
|
||||
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
|
||||
- [ ] Download pages and arbitrary data
|
||||
- [ ] Emoji favicons
|
||||
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
||||
- [ ] Stream support
|
||||
|
@ -225,6 +225,11 @@ func Init() {
|
||||
// An InputField is in focus, nothing should interrupt
|
||||
return event
|
||||
}
|
||||
_, ok = App.GetFocus().(*cview.Modal)
|
||||
if ok {
|
||||
// It's focused on a modal right now, nothing should interrupt
|
||||
return event
|
||||
}
|
||||
|
||||
if tabs[curTab].mode == tabModeDone {
|
||||
// All the keys and operations that can only work while NOT loading
|
||||
|
@ -1,6 +1,8 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -8,11 +10,217 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/makeworld-the-better-one/progressbar/v3"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
||||
// For choosing between download and the portal - copy of YesNo basically
|
||||
var dlChoiceModal = cview.NewModal().
|
||||
SetTextColor(tcell.ColorWhite).
|
||||
SetText("That file could not be displayed. What would you like to do?").
|
||||
AddButtons([]string{"Download", "Open in portal", "Cancel"})
|
||||
|
||||
// Channel to indicate what choice they made using the button text
|
||||
var dlChoiceCh = make(chan string)
|
||||
|
||||
var dlModal = cview.NewModal().
|
||||
SetTextColor(tcell.ColorWhite)
|
||||
|
||||
func dlInit() {
|
||||
if viper.GetBool("a-general.color") {
|
||||
dlChoiceModal.SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetBackgroundColor(tcell.ColorPurple)
|
||||
dlModal.SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetBackgroundColor(tcell.Color130) // DarkOrange3, #af5f00
|
||||
} else {
|
||||
dlChoiceModal.SetButtonBackgroundColor(tcell.ColorWhite).
|
||||
SetButtonTextColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorBlack)
|
||||
dlModal.SetButtonBackgroundColor(tcell.ColorWhite).
|
||||
SetButtonTextColor(tcell.ColorBlack).
|
||||
SetBackgroundColor(tcell.ColorBlack)
|
||||
}
|
||||
|
||||
dlChoiceModal.SetBorder(true)
|
||||
dlChoiceModal.SetBorderColor(tcell.ColorWhite)
|
||||
dlChoiceModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
dlChoiceCh <- buttonLabel
|
||||
})
|
||||
dlChoiceModal.GetFrame().SetTitleColor(tcell.ColorWhite)
|
||||
dlChoiceModal.GetFrame().SetTitleAlign(cview.AlignCenter)
|
||||
|
||||
dlModal.SetBorder(true)
|
||||
dlModal.SetBorderColor(tcell.ColorWhite)
|
||||
dlModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Ok" {
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
}
|
||||
})
|
||||
dlModal.GetFrame().SetTitleColor(tcell.ColorWhite)
|
||||
dlModal.GetFrame().SetTitleAlign(cview.AlignCenter)
|
||||
dlModal.GetFrame().SetTitle(" Download ")
|
||||
}
|
||||
|
||||
// dlChoice displays the download choice modal and acts on the user's choice.
|
||||
// It should run in a goroutine.
|
||||
func dlChoice(u string, resp *gemini.Response) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
Error("URL Error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tabPages.ShowPage("dlChoice")
|
||||
tabPages.SendToFront("dlChoice")
|
||||
App.SetFocus(dlChoiceModal)
|
||||
App.Draw()
|
||||
|
||||
choice := <-dlChoiceCh
|
||||
if choice == "Download" {
|
||||
tabPages.HidePage("dlChoice")
|
||||
App.Draw()
|
||||
downloadURL(u, resp)
|
||||
return
|
||||
}
|
||||
if choice == "Open in portal" {
|
||||
// Open in mozz's proxy
|
||||
portalURL := u
|
||||
if parsed.RawQuery != "" {
|
||||
// Remove query and add encoded version on the end
|
||||
query := parsed.RawQuery
|
||||
parsed.RawQuery = ""
|
||||
portalURL = parsed.String() + "%3F" + query
|
||||
}
|
||||
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
|
||||
handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
App.Draw()
|
||||
return
|
||||
}
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// downloadURL pulls up a modal to show download progress and saves the URL content.
|
||||
// downloadPage should be used for Page content.
|
||||
func downloadURL(u string, resp *gemini.Response) {
|
||||
_, _, width, _ := dlModal.GetInnerRect()
|
||||
// Copy of progressbar.DefaultBytesSilent with custom width
|
||||
bar := progressbar.NewOptions64(
|
||||
-1,
|
||||
progressbar.OptionSetWidth(width),
|
||||
progressbar.OptionSetWriter(ioutil.Discard),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionThrottle(65*time.Millisecond),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSpinnerType(14),
|
||||
)
|
||||
bar.RenderBlank()
|
||||
|
||||
savePath, err := downloadNameFromURL(u, "")
|
||||
if err != nil {
|
||||
Error("Download Error", "Error deciding on file name: "+err.Error())
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
Error("Download Error", "Error creating download file: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
done := false
|
||||
|
||||
go func(isDone *bool) {
|
||||
// Update the bar display
|
||||
for !*isDone {
|
||||
dlModal.SetText(bar.String())
|
||||
App.Draw()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}(&done)
|
||||
|
||||
// Display
|
||||
dlModal.ClearButtons()
|
||||
dlModal.AddButtons([]string{"Downloading..."})
|
||||
tabPages.ShowPage("dl")
|
||||
tabPages.SendToFront("dl")
|
||||
App.SetFocus(dlModal)
|
||||
App.Draw()
|
||||
|
||||
io.Copy(io.MultiWriter(f, bar), resp.Body)
|
||||
done = true
|
||||
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
|
||||
dlModal.ClearButtons()
|
||||
dlModal.AddButtons([]string{"Ok"})
|
||||
dlModal.GetForm().SetFocus(100)
|
||||
App.SetFocus(dlModal)
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// downloadPage saves the passed Page to a file.
|
||||
// It returns the saved path and an error.
|
||||
// It always cleans up, so if an error is returned there is no file saved
|
||||
func downloadPage(p *structs.Page) (string, error) {
|
||||
var savePath string
|
||||
var err error
|
||||
|
||||
if p.Mediatype == structs.TextGemini {
|
||||
savePath, err = downloadNameFromURL(p.Url, ".gmi")
|
||||
} else {
|
||||
savePath, err = downloadNameFromURL(p.Url, ".txt")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644)
|
||||
if err != nil {
|
||||
// Just in case
|
||||
os.Remove(savePath)
|
||||
return "", err
|
||||
}
|
||||
return savePath, err
|
||||
}
|
||||
|
||||
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
|
||||
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
|
||||
// It should include the dot.
|
||||
func downloadNameFromURL(u string, ext string) (string, error) {
|
||||
var name string
|
||||
var err error
|
||||
parsed, _ := url.Parse(u)
|
||||
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
|
||||
// No file, just the root domain
|
||||
name, err = getSafeDownloadName(parsed.Hostname()+ext, true, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
// There's a specific file
|
||||
name = path.Base(parsed.Path)
|
||||
if !strings.Contains(name, ".") {
|
||||
// No extension
|
||||
name += ext
|
||||
}
|
||||
name, err = getSafeDownloadName(name, false, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return filepath.Join(config.DownloadsDir, name), nil
|
||||
}
|
||||
|
||||
// getSafeDownloadName is used by downloads.go only.
|
||||
// It returns a modified name that is unique for the downloads folder.
|
||||
// This way duplicate saved files will not overwrite each other.
|
||||
@ -59,45 +267,3 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||
d.Close()
|
||||
return nn, nil // Name doesn't exist already
|
||||
}
|
||||
|
||||
// downloadPage saves the passed Page to a file.
|
||||
// It returns the saved path and an error.
|
||||
// It always cleans up, so if an error is returned there is no file saved
|
||||
func downloadPage(p *structs.Page) (string, error) {
|
||||
// Figure out file name
|
||||
var name string
|
||||
var err error
|
||||
parsed, _ := url.Parse(p.Url)
|
||||
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
|
||||
// No file, just the root domain
|
||||
if p.Mediatype == structs.TextGemini {
|
||||
name, err = getSafeDownloadName(parsed.Hostname()+".gmi", true, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
name, err = getSafeDownloadName(parsed.Hostname()+".txt", true, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There's a specific file
|
||||
name = path.Base(parsed.Path)
|
||||
if p.Mediatype == structs.TextGemini && !strings.HasSuffix(name, ".gmi") && !strings.HasSuffix(name, ".gemini") {
|
||||
name += ".gmi"
|
||||
}
|
||||
name, err = getSafeDownloadName(name, false, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
savePath := filepath.Join(config.DownloadsDir, name)
|
||||
err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644)
|
||||
if err != nil {
|
||||
// Just in case
|
||||
os.Remove(savePath)
|
||||
return "", err
|
||||
}
|
||||
return savePath, err
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ func modalInit() {
|
||||
AddPage("error", errorModal, false, false).
|
||||
AddPage("input", inputModal, false, false).
|
||||
AddPage("yesno", yesNoModal, false, false).
|
||||
AddPage("bkmk", bkmkModal, false, false)
|
||||
AddPage("bkmk", bkmkModal, false, false).
|
||||
AddPage("dlChoice", dlChoiceModal, false, false).
|
||||
AddPage("dl", dlModal, false, false)
|
||||
|
||||
// Color setup
|
||||
if viper.GetBool("a-general.color") {
|
||||
@ -125,6 +127,7 @@ func modalInit() {
|
||||
yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter)
|
||||
|
||||
bkmkInit()
|
||||
dlInit()
|
||||
}
|
||||
|
||||
// Error displays an error on the screen in a modal.
|
||||
|
@ -166,7 +166,7 @@ func setPage(t *tab, p *structs.Page) {
|
||||
// Setup display
|
||||
App.SetFocus(t.view)
|
||||
|
||||
// Save bottom bar for the tab - TODO: other funcs will apply/display it
|
||||
// Save bottom bar for the tab - other funcs will apply/display it
|
||||
t.barLabel = ""
|
||||
t.barText = p.Url
|
||||
}
|
||||
@ -359,20 +359,7 @@ func handleURL(t *tab, u string) (string, bool) {
|
||||
return ret("", false)
|
||||
}
|
||||
// Status code 20, but not a document that can be displayed
|
||||
yes := YesNo("This type of file can't be displayed. Downloading will be implemented soon. Would like to open the file in a HTTPS proxy for now?")
|
||||
if yes {
|
||||
// Open in mozz's proxy
|
||||
portalURL := u
|
||||
if parsed.RawQuery != "" {
|
||||
// Remove query and add encoded version on the end
|
||||
query := parsed.RawQuery
|
||||
parsed.RawQuery = ""
|
||||
portalURL = parsed.String() + "%3F" + query
|
||||
}
|
||||
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
|
||||
|
||||
handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
|
||||
}
|
||||
go dlChoice(u, res)
|
||||
return ret("", false)
|
||||
}
|
||||
|
||||
|
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
|
||||
github.com/makeworld-the-better-one/go-gemini v0.6.0
|
||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
|
5
go.sum
5
go.sum
@ -107,6 +107,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -123,6 +124,8 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.6.0 h1:wZfeCa8UNRgJrdeNRFBQDBCiekXqA3SJe39I1mboE3E=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.6.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI=
|
||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f/go.mod h1:X6sxWNi9PBgQybpR4fpXPVD5fm7svLqZTQ5DJuERIoM=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
@ -133,6 +136,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
Loading…
Reference in New Issue
Block a user