1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-06-19 19:25:24 +00:00

🚧 Downloading all content seems to work

This commit is contained in:
makeworld 2020-07-10 14:37:18 -04:00
parent 39fa7c6a8b
commit eae118faac
9 changed files with 242 additions and 61 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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.

View File

@ -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
View File

@ -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
View File

@ -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=