mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-10 17:38:51 +00:00
beginnings of a qt gui.
This commit is contained in:
5
view/.gitignore
vendored
Normal file
5
view/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
dist
|
||||
view
|
||||
*.json
|
||||
deploy
|
||||
qtbox
|
||||
13
view/Makefile
Normal file
13
view/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
dist: deploy/linux/nakv deploy/windows/nakv.exe
|
||||
mkdir -p dist
|
||||
cd deploy/linux && tar -czvf nakv_linux.tar.gz nakv
|
||||
mv deploy/linux/nakv_linux.tar.gz dist/
|
||||
rm -f deploy/windows/nakv_windows.zip
|
||||
cd deploy/windows && zip nakv_windows *.exe
|
||||
mv deploy/windows/nakv_windows.zip dist/
|
||||
|
||||
deploy/linux/nakv: $(shell find . -name "*.go")
|
||||
qtdeploy -ldflags="-s -w" -fast build desktop github.com/fiatjaf/nakv
|
||||
|
||||
deploy/windows/nakv.exe: $(shell find . -name "*.go")
|
||||
qtdeploy -ldflags="-s -w" -docker build windows_64_static
|
||||
48
view/helpers.go
Normal file
48
view/helpers.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/keyer"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip46"
|
||||
)
|
||||
|
||||
func handleSecretKeyOrBunker(sec string) (nostr.SecretKey, nostr.Keyer, error) {
|
||||
if strings.HasPrefix(sec, "bunker://") {
|
||||
// it's a bunker
|
||||
bunkerURL := sec
|
||||
clientKey := nostr.Generate()
|
||||
ctx := context.Background()
|
||||
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {})
|
||||
if err != nil {
|
||||
return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err)
|
||||
}
|
||||
|
||||
return nostr.SecretKey{}, keyer.NewBunkerSignerFromBunkerClient(bunker), err
|
||||
}
|
||||
|
||||
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||
return ski.(nostr.SecretKey), nil, nil
|
||||
}
|
||||
|
||||
sk, err := nostr.SecretKeyFromHex(sec)
|
||||
if err != nil {
|
||||
return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err)
|
||||
}
|
||||
|
||||
return sk, keyer.NewPlainKeySigner(sk), nil
|
||||
}
|
||||
|
||||
func decodeTagValue(value string) string {
|
||||
if strings.HasPrefix(value, "npub1") || strings.HasPrefix(value, "nevent1") || strings.HasPrefix(value, "note1") || strings.HasPrefix(value, "nprofile1") || strings.HasPrefix(value, "naddr1") {
|
||||
if ptr, err := nip19.ToPointer(value); err == nil {
|
||||
return ptr.AsTagReference()
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
229
view/main.go
Normal file
229
view/main.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"github.com/therecipe/qt/core"
|
||||
"github.com/therecipe/qt/widgets"
|
||||
)
|
||||
|
||||
var (
|
||||
currentSec nostr.SecretKey
|
||||
currentKeyer nostr.Keyer
|
||||
tagRows [][]*widgets.QLineEdit
|
||||
updateEvent func()
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := widgets.NewQApplication(len(os.Args), os.Args)
|
||||
|
||||
window := widgets.NewQMainWindow(nil, 0)
|
||||
window.SetMinimumSize2(800, 600)
|
||||
window.SetWindowTitle("nakv")
|
||||
|
||||
centralWidget := widgets.NewQWidget(nil, 0)
|
||||
window.SetCentralWidget(centralWidget)
|
||||
|
||||
mainLayout := widgets.NewQVBoxLayout()
|
||||
centralWidget.SetLayout(mainLayout)
|
||||
|
||||
// private key input
|
||||
secLabel := widgets.NewQLabel2("private key (hex or nsec):", nil, 0)
|
||||
mainLayout.AddWidget(secLabel, 0, 0)
|
||||
secEdit := widgets.NewQLineEdit(nil)
|
||||
mainLayout.AddWidget(secEdit, 0, 0)
|
||||
|
||||
secEdit.ConnectTextChanged(func(text string) {
|
||||
if text == "" {
|
||||
currentSec = nostr.SecretKey{}
|
||||
currentKeyer = nil
|
||||
return
|
||||
}
|
||||
sk, bunker, err := handleSecretKeyOrBunker(text)
|
||||
if err != nil {
|
||||
// TODO: have a field somewhere at the bottom of the screen, below the tabs view, to display errors and other messages
|
||||
currentSec = nostr.SecretKey{}
|
||||
currentKeyer = nil
|
||||
return
|
||||
}
|
||||
currentSec = sk
|
||||
currentKeyer = bunker
|
||||
})
|
||||
|
||||
tabWidget := widgets.NewQTabWidget(nil)
|
||||
|
||||
eventTab := widgets.NewQWidget(nil, 0)
|
||||
reqTab := widgets.NewQWidget(nil, 0)
|
||||
|
||||
tabWidget.AddTab(eventTab, "event")
|
||||
tabWidget.AddTab(reqTab, "req")
|
||||
|
||||
mainLayout.AddWidget(tabWidget, 0, 0)
|
||||
|
||||
// set up event tab
|
||||
layout := widgets.NewQVBoxLayout()
|
||||
eventTab.SetLayout(layout)
|
||||
|
||||
// kind input
|
||||
kindHBox := widgets.NewQHBoxLayout()
|
||||
layout.AddLayout(kindHBox, 0)
|
||||
kindLabel := widgets.NewQLabel2("kind:", nil, 0)
|
||||
kindHBox.AddWidget(kindLabel, 0, 0)
|
||||
kindSpin := widgets.NewQSpinBox(nil)
|
||||
// TODO: set default value to 1
|
||||
kindSpin.SetMinimum(0)
|
||||
kindSpin.SetMaximum(1<<16 - 1)
|
||||
kindHBox.AddWidget(kindSpin, 0, 0)
|
||||
kindSpin.ConnectValueChanged(func(int) {
|
||||
updateEvent()
|
||||
})
|
||||
kindNameLabel := widgets.NewQLabel2("", nil, 0)
|
||||
kindHBox.AddWidget(kindNameLabel, 0, 0)
|
||||
|
||||
// content input
|
||||
contentLabel := widgets.NewQLabel2("content:", nil, 0)
|
||||
layout.AddWidget(contentLabel, 0, 0)
|
||||
contentEdit := widgets.NewQTextEdit(nil)
|
||||
layout.AddWidget(contentEdit, 0, 0)
|
||||
contentEdit.ConnectTextChanged(updateEvent)
|
||||
|
||||
// created_at input
|
||||
createdAtLabel := widgets.NewQLabel2("created at:", nil, 0)
|
||||
layout.AddWidget(createdAtLabel, 0, 0)
|
||||
createdAtEdit := widgets.NewQDateTimeEdit(nil)
|
||||
createdAtEdit.SetDateTime(core.QDateTime_CurrentDateTime())
|
||||
layout.AddWidget(createdAtEdit, 0, 0)
|
||||
createdAtEdit.ConnectDateTimeChanged(func(*core.QDateTime) {
|
||||
updateEvent()
|
||||
})
|
||||
|
||||
// tags input
|
||||
tagsLabel := widgets.NewQLabel2("tags:", nil, 0)
|
||||
layout.AddWidget(tagsLabel, 0, 0)
|
||||
tagsLayout := widgets.NewQVBoxLayout()
|
||||
tagRowHBoxes := make([]widgets.QLayout_ITF, 0, 2)
|
||||
layout.AddLayout(tagsLayout, 0)
|
||||
|
||||
var addTagRow func()
|
||||
addTagRow = func() {
|
||||
hbox := widgets.NewQHBoxLayout()
|
||||
tagRowHBoxes = append(tagRowHBoxes, hbox)
|
||||
tagsLayout.AddLayout(hbox, 0)
|
||||
tagItems := []*widgets.QLineEdit{}
|
||||
y := len(tagRows)
|
||||
tagRows = append(tagRows, tagItems)
|
||||
|
||||
var addItem func()
|
||||
addItem = func() {
|
||||
edit := widgets.NewQLineEdit(nil)
|
||||
hbox.AddWidget(edit, 0, 0)
|
||||
x := len(tagItems)
|
||||
tagItems = append(tagItems, edit)
|
||||
tagRows[y] = tagItems
|
||||
edit.ConnectTextChanged(func(text string) {
|
||||
if strings.TrimSpace(text) != "" {
|
||||
// when an item input has been filled check if we have to show more
|
||||
if y == len(tagRows)-1 {
|
||||
addTagRow()
|
||||
}
|
||||
if x == len(tagItems)-1 {
|
||||
addItem()
|
||||
}
|
||||
} else {
|
||||
// do this when an item input has been emptied: check if we need to remove an item from this row
|
||||
nItems := len(tagItems)
|
||||
if nItems >= 2 && strings.TrimSpace(tagItems[nItems-1].Text()) == "" && strings.TrimSpace(tagItems[nItems-2].Text()) == "" {
|
||||
// remove last item if the last 2 are empty
|
||||
hbox.Layout().RemoveWidget(tagItems[nItems-1])
|
||||
tagItems[nItems-1].DeleteLater()
|
||||
tagItems = tagItems[0 : nItems-1]
|
||||
tagRows[y] = tagItems
|
||||
}
|
||||
|
||||
// check if we need to remove rows
|
||||
nRows := len(tagRows)
|
||||
itemIsFilled := func(edit *widgets.QLineEdit) bool { return strings.TrimSpace(edit.Text()) != "" }
|
||||
if nRows >= 2 && !slices.ContainsFunc(tagRows[nRows-1], itemIsFilled) && !slices.ContainsFunc(tagRows[nRows-2], itemIsFilled) {
|
||||
// remove the last row if the last 2 are empty
|
||||
tagsLayout.RemoveItem(tagRowHBoxes[nRows-1])
|
||||
for _, tagItem := range tagRows[nRows-1] {
|
||||
tagItem.DeleteLater()
|
||||
}
|
||||
tagRowHBoxes[nRows-1].QLayout_PTR().DeleteLater()
|
||||
tagRows = tagRows[0 : nRows-1]
|
||||
tagRowHBoxes = tagRowHBoxes[0 : nRows-1]
|
||||
}
|
||||
}
|
||||
updateEvent()
|
||||
})
|
||||
}
|
||||
addItem()
|
||||
}
|
||||
|
||||
// first
|
||||
addTagRow()
|
||||
|
||||
// output JSON
|
||||
outputLabel := widgets.NewQLabel2("event:", nil, 0)
|
||||
layout.AddWidget(outputLabel, 0, 0)
|
||||
outputEdit := widgets.NewQTextEdit(nil)
|
||||
outputEdit.SetReadOnly(true)
|
||||
layout.AddWidget(outputEdit, 0, 0)
|
||||
|
||||
// function to update the display
|
||||
updateEvent = func() {
|
||||
kind := nostr.Kind(kindSpin.Value())
|
||||
kindName := kind.Name()
|
||||
if kindName != "unknown" {
|
||||
kindNameLabel.SetText(kindName)
|
||||
} else {
|
||||
kindNameLabel.SetText("")
|
||||
}
|
||||
tags := make(nostr.Tags, 0, len(tagRows))
|
||||
for y, tagItems := range tagRows {
|
||||
if y == len(tagRows)-1 && strings.TrimSpace(tagItems[0].Text()) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := make(nostr.Tag, 0, len(tagItems))
|
||||
for x, edit := range tagItems {
|
||||
text := strings.TrimSpace(edit.Text())
|
||||
if x == len(tagItems)-1 && text == "" {
|
||||
continue
|
||||
}
|
||||
text = decodeTagValue(text)
|
||||
tag = append(tag, text)
|
||||
}
|
||||
if len(tag) > 0 {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
event := nostr.Event{
|
||||
Kind: kind,
|
||||
Content: contentEdit.ToPlainText(),
|
||||
CreatedAt: nostr.Timestamp(createdAtEdit.DateTime().ToMSecsSinceEpoch() / 1000),
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
if currentKeyer != nil {
|
||||
// TODO:
|
||||
// call debounced function that calls:
|
||||
// currentKeyer.SignEvent(event)
|
||||
// then the json.Marshal / outputEdit.SetPlainText
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.MarshalIndent(event, "", " ")
|
||||
outputEdit.SetPlainText(string(jsonBytes))
|
||||
}
|
||||
|
||||
// initial render
|
||||
updateEvent()
|
||||
|
||||
window.Show()
|
||||
app.Exec()
|
||||
}
|
||||
Reference in New Issue
Block a user