Lightweight single-site browser generator using Webview or Chrome/Chromium.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

243 lines
5.9 KiB

package main
import (
"bytes"
_ "embed"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"github.com/mat/besticon/ico"
"image"
"image/color"
"image/png"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
//go:embed permafrost.png
var defLogo []byte
func createTab(window fyne.Window) *fyne.Container {
// Create entry field for name of SSB
ssbName := widget.NewEntry()
ssbName.SetPlaceHolder("App Name")
// Create entry field for URL of SSB
ssbURL := widget.NewEntry()
ssbURL.SetPlaceHolder("App URL")
// Create dropdown menu for category in desktop file
category := widget.NewSelectEntry([]string{
"AudioVideo",
"Audio",
"Video",
"Development",
"Education",
"Game",
"Graphics",
"Network",
"Office",
"Settings",
"System",
"Utility",
})
category.PlaceHolder = "Category"
// Get default logo and decode as png
img, err := png.Decode(bytes.NewReader(defLogo))
if err != nil {
errDisp(true, err, "Error decoding default logo", window)
}
// Get canvas image from png
defaultIcon := canvas.NewImageFromImage(img)
// Create new container for icon with placeholder line
iconContainer := container.NewMax(canvas.NewLine(color.Black))
// Set default icon in container
setIcon(defaultIcon, iconContainer)
// Create image selection dialog
selectImg := dialog.NewFileOpen(func(file fyne.URIReadCloser, err error) {
// Close file at end of function
defer file.Close()
if err != nil {
errDisp(true, err, "Error opening file", window)
}
// If no file selected, stop further execution of function
if file == nil {
return
}
// Get image from file reader
icon := canvas.NewImageFromReader(file, file.URI().Name())
setIcon(icon, iconContainer)
}, window)
// Create filter constrained to images
selectImg.SetFilter(storage.NewMimeTypeFileFilter([]string{"image/*"}))
// Create button to use favicon as icon
faviconBtn := widget.NewButton("Use favicon", func() {
// Attempt to parse URL
uri, err := url.ParseRequestURI(ssbURL.Text)
if err != nil {
errDisp(true, err, "Error parsing URL. Note that the scheme (https://) is required.", window)
return
}
// Attempt to get favicon using DuckDuckGo API
res, err := http.Get(fmt.Sprintf("https://external-content.duckduckgo.com/ip3/%s.ico", uri.Host))
if err != nil {
errDisp(true, err, "Error getting favicon via DuckDuckGo API", window)
return
}
defer res.Body.Close()
// Attempt to decode data as ico file
favicon, err := ico.Decode(res.Body)
if err != nil {
errDisp(true, err, "Error decoding ico file", window)
return
}
// Get new image from decoded data
icon := canvas.NewImageFromImage(favicon)
setIcon(icon, iconContainer)
})
// Create vertical container
col := container.NewVBox(
category,
widget.NewButton("Select icon", selectImg.Show),
faviconBtn,
)
// Use home directory to get icon path
iconDir := filepath.Join(home, ".config", name, "icons")
useChrome := widget.NewCheck("Use Chrome (not isolated)", nil)
// Create new button that creates SSB
createBtn := widget.NewButton("Create", func() {
// Attempt to create SSB
err := createSSB(ssbName.Text, ssbURL.Text, category.Text, useChrome.Checked, iconContainer.Objects[0].(*canvas.Image).Image)
if err != nil {
errDisp(true, err, "Error creating SSB", window)
}
refreshRmBtns(window, iconDir, rmBtns)
ssbName.SetText("")
ssbURL.SetText("")
category.SetText("")
useChrome.SetChecked(false)
setIcon(defaultIcon, iconContainer)
})
// Create new vertical container
content := container.New(layout.NewVBoxLayout(),
ssbName,
ssbURL,
// Create dual-column container to house icon and fields
container.NewGridWithColumns(2,
container.NewVBox(
container.NewCenter(widget.NewLabel("Icon")),
iconContainer,
useChrome,
),
col,
),
// Add expanding spacer
layout.NewSpacer(),
createBtn,
)
// Return base container
return content
}
func setIcon(img *canvas.Image, logoLayout *fyne.Container) {
// Expand image keeping aspect ratio
img.FillMode = canvas.ImageFillContain
// Set minimum image size to 50x50
img.SetMinSize(fyne.NewSize(50, 50))
// Replace image in layout
logoLayout.Objects[0] = img
// Refresh layout (update changes)
logoLayout.Refresh()
}
func createSSB(ssbName, uri, category string, useChrome bool, icon image.Image) error {
// Parse provided URL for validity
_, err := url.ParseRequestURI(uri)
if err != nil {
return err
}
// Use home directory to get various paths
configDir := filepath.Join(home, ".config", name)
iconDir := filepath.Join(configDir, "icons", ssbName)
desktopDir := filepath.Join(home, ".local", "share", "applications")
// Create paths if nonexistent
err = makeDirs(configDir, iconDir, desktopDir)
if err != nil {
return err
}
// Get paths to resources
iconPath := filepath.Join(iconDir, "icon.png")
desktopPath := filepath.Join(desktopDir, ssbName+".desktop")
// Create icon file
iconFile, err := os.Create(iconPath)
if err != nil {
return err
}
// Encode icon as png, writing to file
err = png.Encode(iconFile, icon)
if err != nil {
return err
}
iconFile.Close()
if useChrome {
uri += " --chrome"
}
// Expand desktop file template with provided data
desktopStr := fmt.Sprintf(DesktopTemplate,
ssbName,
iconPath,
uri,
category,
)
// Create new executable desktop file
desktopFile, err := os.OpenFile(desktopPath, os.O_CREATE|os.O_RDWR, 0755)
if err != nil {
return err
}
// Copy expanded desktop file template to file
_, err = io.Copy(desktopFile, strings.NewReader(desktopStr))
if err != nil {
return err
}
desktopFile.Close()
return nil
}
// Make all directories provided if they do not exist
func makeDirs(dirs ...string) error {
// For each directory
for _, dir := range dirs {
// Create directory and parents if required
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
}
return nil
}