mirror of
https://code.rocketnine.space/tslocum/gmitohtml.git
synced 2024-11-01 02:25:04 +01:00
Support bookmarks
This commit is contained in:
parent
7445d6c830
commit
1c986eb018
7 changed files with 329 additions and 61 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
1.0.2:
|
||||||
|
- Support bookmarks
|
||||||
|
|
||||||
1.0.1:
|
1.0.1:
|
||||||
- Add option to allow local file access
|
- Add option to allow local file access
|
||||||
- Display navigation bar in pages
|
- Display navigation bar in pages
|
||||||
|
|
|
@ -3,6 +3,14 @@ default. You may specify a different location via the `--config` argument.
|
||||||
|
|
||||||
# Configuration options
|
# Configuration options
|
||||||
|
|
||||||
|
## Bookmarks
|
||||||
|
|
||||||
|
Bookmarks are defined as a list of URLs and corresponding label.
|
||||||
|
|
||||||
|
Defining bookmarks manually via configuration file is possible, however it is
|
||||||
|
not required as the gmitohtml configuration file is updated when bookmarks are
|
||||||
|
modified using the web interface.
|
||||||
|
|
||||||
## Client certificates
|
## Client certificates
|
||||||
|
|
||||||
Client certificates may be specified via the `Certs` option.
|
Client certificates may be specified via the `Certs` option.
|
||||||
|
@ -30,6 +38,10 @@ For example, to view `/home/dioscuri/sites/gemlog/index.gmi`, navigate to
|
||||||
# Example config.yaml
|
# Example config.yaml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
bookmarks:
|
||||||
|
gemini://gemini.circumlunar.space/: Gemini protocol
|
||||||
|
gemini://gus.guru/: GUS - Gemini Universal Search
|
||||||
|
|
||||||
certs:
|
certs:
|
||||||
astrobotany.mozz.us:
|
astrobotany.mozz.us:
|
||||||
cert: /home/dioscuri/.config/gmitohtml/astrobotany.mozz.us.crt
|
cert: /home/dioscuri/.config/gmitohtml/astrobotany.mozz.us.crt
|
||||||
|
|
33
config.go
33
config.go
|
@ -3,8 +3,12 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"gitlab.com/tslocum/gmitohtml/pkg/gmitohtml"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,13 +20,25 @@ type certConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type appConfig struct {
|
type appConfig struct {
|
||||||
|
Bookmarks map[string]string
|
||||||
|
|
||||||
Certs map[string]*certConfig
|
Certs map[string]*certConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = &appConfig{
|
var config = &appConfig{
|
||||||
|
Bookmarks: make(map[string]string),
|
||||||
|
|
||||||
Certs: make(map[string]*certConfig),
|
Certs: make(map[string]*certConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultConfigPath() string {
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err == nil && homedir != "" {
|
||||||
|
return path.Join(homedir, ".config", "gmitohtml", "config.yaml")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func readconfig(configPath string) error {
|
func readconfig(configPath string) error {
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
return errors.New("file unspecified")
|
return errors.New("file unspecified")
|
||||||
|
@ -42,3 +58,20 @@ func readconfig(configPath string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveConfig(configPath string) error {
|
||||||
|
config.Bookmarks = gmitohtml.GetBookmarks()
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal configuration: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.MkdirAll(path.Dir(configPath), 0755) // Ignore error
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(configPath, out, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save configuration to %s: %s", configPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
36
main.go
36
main.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"gitlab.com/tslocum/gmitohtml/pkg/gmitohtml"
|
"gitlab.com/tslocum/gmitohtml/pkg/gmitohtml"
|
||||||
|
@ -43,20 +42,26 @@ func main() {
|
||||||
// TODO option to include response header in page
|
// TODO option to include response header in page
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
defaultConfig := defaultConfigPath()
|
||||||
if configFile == "" {
|
if configFile == "" {
|
||||||
homedir, err := os.UserHomeDir()
|
configFile = defaultConfig
|
||||||
if err == nil && homedir != "" {
|
|
||||||
defaultConfig := path.Join(homedir, ".config", "gmitohtml", "config.yaml")
|
|
||||||
if _, err := os.Stat(defaultConfig); !os.IsNotExist(err) {
|
|
||||||
configFile = defaultConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
err := readconfig(configFile)
|
var configExists bool
|
||||||
if err != nil {
|
if _, err := os.Stat(defaultConfig); !os.IsNotExist(err) {
|
||||||
log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring gmitohtml", configFile, err)
|
configExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if configExists || configFile != defaultConfig {
|
||||||
|
err := readconfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring gmitohtml", configFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for u, label := range config.Bookmarks {
|
||||||
|
gmitohtml.AddBookmark(u, label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +83,15 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if daemon != "" {
|
if daemon != "" {
|
||||||
|
gmitohtml.SetOnBookmarksChanged(func() {
|
||||||
|
config.Bookmarks = gmitohtml.GetBookmarks()
|
||||||
|
|
||||||
|
err := saveConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
err := gmitohtml.StartDaemon(daemon, allowFile)
|
err := gmitohtml.StartDaemon(daemon, allowFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
|
@ -12,22 +12,15 @@ const pageHeader = `
|
||||||
<body>
|
<body>
|
||||||
<form method="post" action="/" novalidate>
|
<form method="post" action="/" novalidate>
|
||||||
<input type="url" name="address" placeholder="Address" size="40" value="~GEMINICURRENTURL~" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" css="width: 100%;" ~GEMINIAUTOFOCUS~>
|
<input type="url" name="address" placeholder="Address" size="40" value="~GEMINICURRENTURL~" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" css="width: 100%;" ~GEMINIAUTOFOCUS~>
|
||||||
</form>
|
</form>`
|
||||||
<br>`
|
|
||||||
|
|
||||||
const pageFooter = `
|
const navigationMenuText = `
|
||||||
</body>
|
<div style="padding-left: 84px;padding-bottom: 7px;">
|
||||||
</html>
|
<a href="/bookmarks" class="navlink">View bookmarks</a> - <a href="/bookmarks?add=~GEMINICURRENTURL~" class="navlink">Add bookmark</a>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
const indexPage = pageHeader + `
|
const inputPrompt = `
|
||||||
<ul>
|
|
||||||
<li><a href="/gemini/gus.guru/">GUS - Gemini Universal Search</a></li>
|
|
||||||
<li><a href="/gemini/gemini.circumlunar.space/">Gemini protocol</a></li>
|
|
||||||
</ul>
|
|
||||||
` + pageFooter
|
|
||||||
|
|
||||||
const inputPage = pageHeader + `
|
|
||||||
<form method="post" action="~GEMINIINPUTFORM~">
|
<form method="post" action="~GEMINIINPUTFORM~">
|
||||||
<div style="padding-top: 25px;">
|
<div style="padding-top: 25px;">
|
||||||
<span style="font-size: 1.5em;">~GEMINIINPUTPROMPT~</span><br><br>
|
<span style="font-size: 1.5em;">~GEMINIINPUTPROMPT~</span><br><br>
|
||||||
|
@ -36,7 +29,12 @@ const inputPage = pageHeader + `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
` + pageFooter
|
`
|
||||||
|
|
||||||
|
const pageFooter = `
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
func loadAssets() {
|
func loadAssets() {
|
||||||
fs["/assets/style.css"] = loadFile("style.css", `
|
fs["/assets/style.css"] = loadFile("style.css", `
|
||||||
|
@ -123,6 +121,34 @@ a {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
color: #FF0000;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #551A8B;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.navlink {
|
||||||
|
color: #0000EE;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a.navlink:hover,
|
||||||
|
a.navlink:focus,
|
||||||
|
a.navlink:active {
|
||||||
|
color: #FF0000;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a.navlink:visited {
|
||||||
|
color: #0000EE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Remove the bottom border in Chrome 57-
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
@ -395,37 +421,51 @@ template {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgb(26, 168, 245);
|
color: rgb(26, 168, 245);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover,
|
a:hover,
|
||||||
a:focus,
|
a:focus,
|
||||||
a:active {
|
a:active {
|
||||||
color: rgb(24, 151, 219);
|
color: rgb(24, 151, 219);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
a:visited {
|
a:visited {
|
||||||
color: rgb(200, 118, 255);
|
color: rgb(200, 118, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
a.navlink {
|
||||||
background-color: black;
|
color: rgb(26, 168, 245);
|
||||||
color: white;
|
text-decoration: none;
|
||||||
border-color: gray;
|
}
|
||||||
border-width: 0.3em;
|
a.navlink:hover,
|
||||||
border-style: solid;
|
a.navlink:focus,
|
||||||
padding: 0.5em;
|
a.navlink:active {
|
||||||
}
|
color: rgb(24, 151, 219);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a.navlink:visited {
|
||||||
|
color: rgb(26, 168, 245);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border-color: gray;
|
||||||
|
border-width: 0.3em;
|
||||||
|
border-style: solid;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`, fs)
|
`, fs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,13 @@ func rewriteURL(u string, loc *url.URL) string {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func navigationMenu() []byte {
|
||||||
|
if daemonAddress == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []byte(navigationMenuText)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert converts text/gemini to text/html.
|
// Convert converts text/gemini to text/html.
|
||||||
func Convert(page []byte, u string) []byte {
|
func Convert(page []byte, u string) []byte {
|
||||||
var result []byte
|
var result []byte
|
||||||
|
@ -148,7 +155,9 @@ func Convert(page []byte, u string) []byte {
|
||||||
result = append(result, []byte("</pre>\n")...)
|
result = append(result, []byte("</pre>\n")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append([]byte(pageHeader), result...)
|
data := []byte(pageHeader)
|
||||||
result = append(result, []byte(pageFooter)...)
|
data = append(data, navigationMenu()...)
|
||||||
return fillTemplateVariables(result, u, false)
|
data = append(data, result...)
|
||||||
|
data = append(data, []byte(pageFooter)...)
|
||||||
|
return fillTemplateVariables(data, u, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -19,14 +20,36 @@ import (
|
||||||
var lastRequestTime = time.Now().Unix()
|
var lastRequestTime = time.Now().Unix()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
clientCerts = make(map[string]tls.Certificate)
|
clientCerts = make(map[string]tls.Certificate)
|
||||||
allowFileAccess bool
|
bookmarks = make(map[string]string)
|
||||||
|
bookmarksSorted []string
|
||||||
|
allowFileAccess bool
|
||||||
|
onBookmarksChanged func()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultBookmarks = map[string]string{
|
||||||
|
"gemini://gemini.circumlunar.space/": "Project Gemini",
|
||||||
|
"gemini://gus.guru/": "GUS - Gemini Universal Search",
|
||||||
|
}
|
||||||
|
|
||||||
// ErrInvalidCertificate is the error returned when an invalid certificate is provided.
|
// ErrInvalidCertificate is the error returned when an invalid certificate is provided.
|
||||||
var ErrInvalidCertificate = errors.New("invalid certificate")
|
var ErrInvalidCertificate = errors.New("invalid certificate")
|
||||||
|
|
||||||
// Fetch downloads and converts a Gemini page.
|
func bookmarksList() []byte {
|
||||||
|
fakeURL, _ := url.Parse("/") // Always succeeds
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.Write([]byte(`<div style="padding-left: 12px;">`))
|
||||||
|
b.Write([]byte(`<br><a href="/bookmarks" class="navlink">Bookmarks</a><br></div>`))
|
||||||
|
b.Write([]byte(`<ul>`))
|
||||||
|
for _, u := range bookmarksSorted {
|
||||||
|
b.Write([]byte(fmt.Sprintf(`<li><a href="%s">%s</a></li>`, rewriteURL(u, fakeURL), bookmarks[u])))
|
||||||
|
}
|
||||||
|
b.Write([]byte("</ul>"))
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch downloads and converts a Gemini page.
|
||||||
func fetch(u string) ([]byte, []byte, error) {
|
func fetch(u string) ([]byte, []byte, error) {
|
||||||
if u == "" {
|
if u == "" {
|
||||||
return nil, nil, ErrInvalidURL
|
return nil, nil, ErrInvalidURL
|
||||||
|
@ -94,7 +117,10 @@ func fetch(u string) ([]byte, []byte, error) {
|
||||||
if requestInput {
|
if requestInput {
|
||||||
requestSensitiveInput := bytes.HasPrefix(header, []byte("11"))
|
requestSensitiveInput := bytes.HasPrefix(header, []byte("11"))
|
||||||
|
|
||||||
data = []byte(inputPage)
|
data = []byte(pageHeader)
|
||||||
|
data = append(data, navigationMenu()...)
|
||||||
|
|
||||||
|
data = append(data, []byte(inputPrompt)...)
|
||||||
|
|
||||||
data = bytes.Replace(data, []byte("~GEMINIINPUTFORM~"), []byte(html.EscapeString(rewriteURL(u, requestURL))), 1)
|
data = bytes.Replace(data, []byte("~GEMINIINPUTFORM~"), []byte(html.EscapeString(rewriteURL(u, requestURL))), 1)
|
||||||
|
|
||||||
|
@ -133,7 +159,12 @@ func handleIndex(writer http.ResponseWriter, request *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write(fillTemplateVariables([]byte(indexPage), request.URL.String(), true))
|
var page []byte
|
||||||
|
page = append(page, pageHeader...)
|
||||||
|
page = append(page, bookmarksList()...)
|
||||||
|
page = append(page, pageFooter...)
|
||||||
|
|
||||||
|
writer.Write(fillTemplateVariables(page, request.URL.String(), true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillTemplateVariables(data []byte, currentURL string, autofocus bool) []byte {
|
func fillTemplateVariables(data []byte, currentURL string, autofocus bool) []byte {
|
||||||
|
@ -242,6 +273,81 @@ func handleAssets(writer http.ResponseWriter, request *http.Request) {
|
||||||
http.FileServer(fs).ServeHTTP(writer, request)
|
http.FileServer(fs).ServeHTTP(writer, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleBookmarks(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
postAddress := request.PostFormValue("address")
|
||||||
|
postLabel := request.PostFormValue("label")
|
||||||
|
if postLabel == "" && postAddress != "" {
|
||||||
|
postLabel = postAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
editBookmark := request.FormValue("edit")
|
||||||
|
if editBookmark != "" {
|
||||||
|
if postLabel == "" {
|
||||||
|
label, ok := bookmarks[editBookmark]
|
||||||
|
if !ok {
|
||||||
|
writer.Write([]byte("<h1>Error: bookmark not found</h1>"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(pageHeader)
|
||||||
|
|
||||||
|
data = append(data, []byte(fmt.Sprintf(`<br><form method="post" action="%s"><h3>Edit bookmark</h3><input type="text" size="40" name="address" placeholder="Address" value="%s" autofocus><br><br><input type="text" size="40" name="label" placeholder="Label" value="%s"><br><br><input type="submit" value="Update"></form>`, request.URL.Path+"?"+request.URL.RawQuery, html.EscapeString(editBookmark), html.EscapeString(label)))...)
|
||||||
|
|
||||||
|
data = append(data, []byte(pageFooter)...)
|
||||||
|
|
||||||
|
writer.Write(fillTemplateVariables(data, "", false))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if editBookmark != postAddress || bookmarks[editBookmark] != postLabel {
|
||||||
|
RemoveBookmark(editBookmark)
|
||||||
|
AddBookmark(postAddress, postLabel)
|
||||||
|
}
|
||||||
|
} else if postLabel != "" {
|
||||||
|
AddBookmark(postAddress, postLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteBookmark := request.FormValue("delete")
|
||||||
|
if deleteBookmark != "" {
|
||||||
|
RemoveBookmark(deleteBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(pageHeader)
|
||||||
|
|
||||||
|
addBookmark := request.FormValue("add")
|
||||||
|
|
||||||
|
addressFocus := "autofocus"
|
||||||
|
labelFocus := ""
|
||||||
|
if addBookmark != "" {
|
||||||
|
addressFocus = ""
|
||||||
|
labelFocus = "autofocus"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, []byte(fmt.Sprintf(`<br><form method="post" action="/bookmarks"><h3>Add bookmark</h3><input type="text" size="40" name="address" placeholder="Address" value="%s" %s><br><br><input type="text" size="40" name="label" placeholder="Label" %s><br><br><input type="submit" value="Add"></form>`, html.EscapeString(addBookmark), addressFocus, labelFocus))...)
|
||||||
|
|
||||||
|
if len(bookmarks) > 0 && addBookmark == "" {
|
||||||
|
fakeURL, _ := url.Parse("/") // Always succeeds
|
||||||
|
|
||||||
|
data = append(data, []byte(`<br><h3>Bookmarks</h3><table border="1" cellpadding="5">`)...)
|
||||||
|
for _, u := range bookmarksSorted {
|
||||||
|
data = append(data, []byte(fmt.Sprintf(`<tr><td>%s<br><a href="%s">%s</a></td><td><a href="/bookmarks?edit=%s" class="navlink">Edit</a></td><td><a href="/bookmarks?delete=%s" onclick="return confirm('Are you sure you want to delete this bookmark?')" class="navlink">Delete</a></td></tr>`, html.EscapeString(bookmarks[u]), html.EscapeString(rewriteURL(u, fakeURL)), html.EscapeString(u), html.EscapeString(url.PathEscape(u)), html.EscapeString(url.PathEscape(u))))...)
|
||||||
|
}
|
||||||
|
data = append(data, []byte(`</table>`)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, []byte(pageFooter)...)
|
||||||
|
|
||||||
|
writer.Write(fillTemplateVariables(data, "", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnBookmarksChanged sets the function called when a bookmark is changed.
|
||||||
|
func SetOnBookmarksChanged(f func()) {
|
||||||
|
onBookmarksChanged = f
|
||||||
|
}
|
||||||
|
|
||||||
// StartDaemon starts the page conversion daemon.
|
// StartDaemon starts the page conversion daemon.
|
||||||
func StartDaemon(address string, allowFile bool) error {
|
func StartDaemon(address string, allowFile bool) error {
|
||||||
daemonAddress = address
|
daemonAddress = address
|
||||||
|
@ -249,8 +355,15 @@ func StartDaemon(address string, allowFile bool) error {
|
||||||
|
|
||||||
loadAssets()
|
loadAssets()
|
||||||
|
|
||||||
|
if len(bookmarks) == 0 {
|
||||||
|
for u, label := range defaultBookmarks {
|
||||||
|
AddBookmark(u, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler := http.NewServeMux()
|
handler := http.NewServeMux()
|
||||||
handler.HandleFunc("/assets/style.css", handleAssets)
|
handler.HandleFunc("/assets/style.css", handleAssets)
|
||||||
|
handler.HandleFunc("/bookmarks", handleBookmarks)
|
||||||
handler.HandleFunc("/", handleRequest)
|
handler.HandleFunc("/", handleRequest)
|
||||||
go func() {
|
go func() {
|
||||||
log.Fatal(http.ListenAndServe(address, handler))
|
log.Fatal(http.ListenAndServe(address, handler))
|
||||||
|
@ -284,3 +397,47 @@ func SetClientCertificate(domain string, certificate []byte, privateKey []byte)
|
||||||
clientCerts[domain] = clientCert
|
clientCerts[domain] = clientCert
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddBookmark adds a bookmark.
|
||||||
|
func AddBookmark(u string, label string) {
|
||||||
|
parsed, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if parsed.Scheme == "" {
|
||||||
|
parsed.Scheme = "gemini"
|
||||||
|
}
|
||||||
|
parsed.Host = strings.ToLower(parsed.Host)
|
||||||
|
|
||||||
|
bookmarks[parsed.String()] = label
|
||||||
|
|
||||||
|
bookmarksUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBookmarks returns all bookmarks.
|
||||||
|
func GetBookmarks() map[string]string {
|
||||||
|
return bookmarks
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveBookmark removes a bookmark.
|
||||||
|
func RemoveBookmark(u string) {
|
||||||
|
delete(bookmarks, u)
|
||||||
|
|
||||||
|
bookmarksUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
func bookmarksUpdated() {
|
||||||
|
var allURLs []string
|
||||||
|
for u := range bookmarks {
|
||||||
|
allURLs = append(allURLs, u)
|
||||||
|
}
|
||||||
|
sort.Slice(allURLs, func(i, j int) bool {
|
||||||
|
return strings.ToLower(bookmarks[allURLs[i]]) < strings.ToLower(bookmarks[allURLs[j]])
|
||||||
|
})
|
||||||
|
|
||||||
|
bookmarksSorted = allURLs
|
||||||
|
|
||||||
|
if onBookmarksChanged != nil {
|
||||||
|
onBookmarksChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue