Compare commits
4 Commits
feature/ui
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a842645c88 | ||
| 89399a9a33 | |||
|
|
a34cd1005d | ||
| f7d2a7fafc |
16
README.md
16
README.md
@ -28,9 +28,13 @@ Vlastnosti:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rychlý start (gui)
|
## Rychlý start (GUI)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 1) po buildnutí (go build .) stačí spustit binárku – otevře se GUI
|
||||||
|
./fckeuspy-go
|
||||||
|
|
||||||
|
# Alternativně přes go run
|
||||||
go run . gui
|
go run . gui
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -133,11 +137,15 @@ Formát payloadu:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fyne GUI režim
|
## Spuštění (GUI / Web)
|
||||||
|
|
||||||
Spuštění desktopu:
|
Spuštění desktopu (GUI):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# po buildnutí (go build .)
|
||||||
|
./fckeuspy-go
|
||||||
|
|
||||||
|
# nebo přes go run
|
||||||
go run . gui
|
go run . gui
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -153,6 +161,8 @@ HTTP režim zůstává:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . web
|
go run . web
|
||||||
|
# nebo po buildnutí (go build .)
|
||||||
|
./fckeuspy-go web
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
4
cmd.go
4
cmd.go
@ -10,6 +10,10 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "fckeuspy",
|
Use: "fckeuspy",
|
||||||
Short: "Hybrid RSA+AES šifrovací nástroj (GUI / HTTP)",
|
Short: "Hybrid RSA+AES šifrovací nástroj (GUI / HTTP)",
|
||||||
Long: "Fckeuspy: nástroj pro šifrování a dešifrování pomocí hybridního RSA-OAEP + AES-GCM, s Fyne GUI a HTTP serverem.",
|
Long: "Fckeuspy: nástroj pro šifrování a dešifrování pomocí hybridního RSA-OAEP + AES-GCM, s Fyne GUI a HTTP serverem.",
|
||||||
|
// Default: run GUI when executed without subcommand
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runFyne()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var guiCmd = &cobra.Command{
|
var guiCmd = &cobra.Command{
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
|
||||||
"math"
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
@ -32,17 +31,12 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
|
|||||||
|
|
||||||
// DecodeQR decodes first QR code text from an image.
|
// DecodeQR decodes first QR code text from an image.
|
||||||
func DecodeQR(img image.Image) (string, error) {
|
func DecodeQR(img image.Image) (string, error) {
|
||||||
log.Printf("[qr] start decode: %dx%d %T", img.Bounds().Dx(), img.Bounds().Dy(), img)
|
|
||||||
// Try basic decode first
|
// Try basic decode first
|
||||||
codes, err := goqr.Recognize(img)
|
codes, err := goqr.Recognize(img)
|
||||||
if err == nil && len(codes) > 0 {
|
if err == nil && len(codes) > 0 {
|
||||||
log.Printf("[qr] basic success length=%d", len(codes))
|
|
||||||
return string(codes[0].Payload), nil
|
return string(codes[0].Payload), nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[qr] basic error: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("[qr] basic no codes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert palette/indexed images to RGBA first to avoid issues with goqr
|
// Convert palette/indexed images to RGBA first to avoid issues with goqr
|
||||||
@ -55,14 +49,8 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
// Try again after conversion
|
// Try again after conversion
|
||||||
codes, err := goqr.Recognize(img)
|
codes, err := goqr.Recognize(img)
|
||||||
if err == nil && len(codes) > 0 {
|
if err == nil && len(codes) > 0 {
|
||||||
log.Printf("[qr] palette-convert success")
|
|
||||||
return string(codes[0].Payload), nil
|
return string(codes[0].Payload), nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
log.Printf("[qr] palette-convert err: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("[qr] palette-convert no codes")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
|
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
|
||||||
@ -213,20 +201,17 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for idx, attempt := range attempts {
|
for _, attempt := range attempts {
|
||||||
codes, err := goqr.Recognize(attempt)
|
codes, err := goqr.Recognize(attempt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if firstErr == nil {
|
if firstErr == nil {
|
||||||
firstErr = err
|
firstErr = err
|
||||||
}
|
}
|
||||||
log.Printf("[qr] attempt %d error: %v", idx, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(codes) > 0 {
|
if len(codes) > 0 {
|
||||||
log.Printf("[qr] attempt %d success (%d codes)", idx, len(codes))
|
|
||||||
return string(codes[0].Payload), nil
|
return string(codes[0].Payload), nil
|
||||||
}
|
}
|
||||||
log.Printf("[qr] attempt %d no codes", idx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZXing helper used across multiple branches
|
// ZXing helper used across multiple branches
|
||||||
@ -247,25 +232,19 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Also try ZXing on all preprocessed attempts
|
// Also try ZXing on all preprocessed attempts
|
||||||
for idx, attempt := range attempts {
|
for _, attempt := range attempts {
|
||||||
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
||||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||||
}); err == nil && txt != "" {
|
}); err == nil && txt != "" {
|
||||||
log.Printf("[qr] zxing attempt %d success (pure)", idx)
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
||||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||||
}); err == nil && txt != "" {
|
}); err == nil && txt != "" {
|
||||||
log.Printf("[qr] zxing attempt %d success (try-harder)", idx)
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if firstErr != nil {
|
|
||||||
log.Printf("[qr] preprocess firstErr: %v", firstErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
|
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
|
||||||
addQuietZone := func(src image.Image) image.Image {
|
addQuietZone := func(src image.Image) image.Image {
|
||||||
b := src.Bounds()
|
b := src.Bounds()
|
||||||
@ -315,7 +294,6 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
modified = append(modified, addQuietZone(invert(base)))
|
modified = append(modified, addQuietZone(invert(base)))
|
||||||
for _, m := range modified {
|
for _, m := range modified {
|
||||||
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
|
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
|
||||||
log.Printf("[qr] quiet-zone/invert success")
|
|
||||||
return string(codes[0].Payload), nil
|
return string(codes[0].Payload), nil
|
||||||
}
|
}
|
||||||
// Also try ZXing on these variants before moving on
|
// Also try ZXing on these variants before moving on
|
||||||
@ -323,13 +301,11 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||||
}); err == nil && txt != "" {
|
}); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (quiet-zone+pure)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
|
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
|
||||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||||
}); err == nil && txt != "" {
|
}); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (quiet-zone+try-harder)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,27 +316,19 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
|
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (hints)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("[qr] gozxing(hints) err: %v", err)
|
|
||||||
}
|
}
|
||||||
// Try again without PURE_BARCODE in case quiet zone is cropped
|
// Try again without PURE_BARCODE in case quiet zone is cropped
|
||||||
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
|
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
|
||||||
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
|
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (try-harder)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("[qr] gozxing(try-harder) err: %v", err)
|
|
||||||
}
|
}
|
||||||
// Try ZXing on rotations as well
|
// Try ZXing on rotations as well
|
||||||
for _, r := range rotate(base) {
|
for _, r := range rotate(base) {
|
||||||
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
|
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (rotated)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
|
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing success (rotated pure)")
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -392,7 +360,6 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
tryPure := func(src image.Image) (string, bool) {
|
tryPure := func(src image.Image) (string, bool) {
|
||||||
for _, px := range []int{1, 2, 3} {
|
for _, px := range []int{1, 2, 3} {
|
||||||
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
|
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing pure+inset %d success", px)
|
|
||||||
return txt, true
|
return txt, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -425,11 +392,9 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
|
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing pure+asym inset %d,%d,%d,%d success", l, t, r, btm)
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
|
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing asym inset %d,%d,%d,%d success", l, t, r, btm)
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,7 +430,6 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
if minX < maxX && minY < maxY {
|
if minX < maxX && minY < maxY {
|
||||||
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
|
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
|
||||||
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
|
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
|
||||||
log.Printf("[qr] crop bbox size=%dx%d", crop.Bounds().Dx(), crop.Bounds().Dy())
|
|
||||||
for _, up := range []int{2, 3, 4} {
|
for _, up := range []int{2, 3, 4} {
|
||||||
cw := crop.Bounds().Dx() * up
|
cw := crop.Bounds().Dx() * up
|
||||||
ch := crop.Bounds().Dy() * up
|
ch := crop.Bounds().Dy() * up
|
||||||
@ -481,10 +445,7 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
|
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
|
||||||
log.Printf("[qr] gozxing crop+scale success up=%d", up)
|
|
||||||
return txt, nil
|
return txt, nil
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("[qr] crop up=%d fail: %v", up, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -499,12 +460,8 @@ func DecodeQR(img image.Image) (string, error) {
|
|||||||
c.Stdin = bytes.NewReader(buf.Bytes())
|
c.Stdin = bytes.NewReader(buf.Bytes())
|
||||||
out, err := c.Output()
|
out, err := c.Output()
|
||||||
if err == nil && len(out) > 0 {
|
if err == nil && len(out) > 0 {
|
||||||
log.Printf("[qr] zbarimg success")
|
|
||||||
return string(out), nil
|
return string(out), nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
log.Printf("[qr] zbarimg error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", errors.New("no qr code found")
|
return "", errors.New("no qr code found")
|
||||||
|
|||||||
190
ui.go
190
ui.go
@ -10,7 +10,6 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -55,6 +54,7 @@ func buildEntries() *uiParts {
|
|||||||
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
|
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
|
||||||
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
|
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
|
||||||
p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
|
p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
|
||||||
|
p.pubQR.FillMode = canvas.ImageFillContain
|
||||||
p.toastLabel.Hide()
|
p.toastLabel.Hide()
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@ -74,11 +74,16 @@ func (p *uiParts) showToast(s string) {
|
|||||||
type simpleTheme struct{}
|
type simpleTheme struct{}
|
||||||
|
|
||||||
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
|
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
|
||||||
if n == theme.ColorNameBackground {
|
switch n {
|
||||||
|
case theme.ColorNameBackground:
|
||||||
return color.NRGBA{24, 27, 31, 255}
|
return color.NRGBA{24, 27, 31, 255}
|
||||||
}
|
case theme.ColorNameDisabled:
|
||||||
|
// Make disabled text brighter for readability on dark background
|
||||||
|
return color.NRGBA{230, 233, 238, 255}
|
||||||
|
default:
|
||||||
return theme.DefaultTheme().Color(n, v)
|
return theme.DefaultTheme().Color(n, v)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(st) }
|
func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(st) }
|
||||||
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) }
|
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) }
|
||||||
func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) }
|
func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) }
|
||||||
@ -172,7 +177,6 @@ func readImageClipboard() (image.Image, error) {
|
|||||||
|
|
||||||
if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
|
if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
|
||||||
if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
|
if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
|
||||||
log.Printf("[clip] wl types: %s", strings.TrimSpace(string(types)))
|
|
||||||
pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
|
pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, p := range pref {
|
for _, p := range pref {
|
||||||
@ -190,23 +194,13 @@ func readImageClipboard() (image.Image, error) {
|
|||||||
}
|
}
|
||||||
for _, t := range order {
|
for _, t := range order {
|
||||||
if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
|
if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
|
||||||
log.Printf("[clip] got type %s size=%d", t, len(data))
|
|
||||||
if img, ok := tryDecode(data); ok {
|
if img, ok := tryDecode(data); ok {
|
||||||
// save debug
|
|
||||||
_ = os.MkdirAll("qr_debug", 0o755)
|
|
||||||
fn := filepath.Join("qr_debug", fmt.Sprintf("clip_raw_%d.png", time.Now().UnixNano()))
|
|
||||||
if f, e := os.Create(fn); e == nil {
|
|
||||||
_ = png.Encode(f, img)
|
|
||||||
f.Close()
|
|
||||||
log.Printf("[clip] saved %s", fn)
|
|
||||||
}
|
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if data, err := exec.Command("wl-paste").Output(); err == nil {
|
if data, err := exec.Command("wl-paste").Output(); err == nil {
|
||||||
log.Printf("[clip] generic wl-paste size=%d", len(data))
|
|
||||||
if img, ok := tryDecode(data); ok {
|
if img, ok := tryDecode(data); ok {
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
@ -215,14 +209,12 @@ func readImageClipboard() (image.Image, error) {
|
|||||||
if has("xclip") {
|
if has("xclip") {
|
||||||
for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
|
for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
|
||||||
if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
|
if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
|
||||||
log.Printf("[clip] xclip type %s size=%d", m, len(data))
|
|
||||||
if img, ok := tryDecode(data); ok {
|
if img, ok := tryDecode(data); ok {
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
|
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
|
||||||
log.Printf("[clip] xclip raw size=%d", len(data))
|
|
||||||
if img, ok := tryDecode(data); ok {
|
if img, ok := tryDecode(data); ok {
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
@ -234,18 +226,19 @@ func readImageClipboard() (image.Image, error) {
|
|||||||
// Identity tab
|
// Identity tab
|
||||||
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||||
// Toolbar: choose what to encode into the QR to keep density manageable
|
// Toolbar: choose what to encode into the QR to keep density manageable
|
||||||
mode := "combined" // combined, cert, pub
|
mode := "cert" // cert, pub
|
||||||
chooser := widget.NewSelect([]string{"Veřejný+Cert", "Jen Cert", "Jen Veřejný"}, func(string) {})
|
options := []string{"Certifikát", "Veřejný klíč"}
|
||||||
chooser.Selected = "Veřejný+Cert"
|
chooser := widget.NewSelect(options, func(string) {})
|
||||||
|
chooser.Selected = options[0]
|
||||||
var update func()
|
var update func()
|
||||||
chooser.OnChanged = func(v string) {
|
chooser.OnChanged = func(v string) {
|
||||||
switch v {
|
switch v {
|
||||||
case "Jen Cert":
|
case "Certifikát":
|
||||||
mode = "cert"
|
mode = "cert"
|
||||||
case "Jen Veřejný":
|
case "Veřejný klíč":
|
||||||
mode = "pub"
|
mode = "pub"
|
||||||
default:
|
default:
|
||||||
mode = "combined"
|
mode = "cert"
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
@ -267,6 +260,8 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
d.Resize(fyne.NewSize(420, 200))
|
d.Resize(fyne.NewSize(420, 200))
|
||||||
d.Show()
|
d.Show()
|
||||||
})
|
})
|
||||||
|
// Keep button minimal; align right
|
||||||
|
deleteRow := container.NewHBox(layout.NewSpacer(), deleteBtn)
|
||||||
makeQR := func(data string, target *canvas.Image) {
|
makeQR := func(data string, target *canvas.Image) {
|
||||||
if data == "" {
|
if data == "" {
|
||||||
target.Image = nil
|
target.Image = nil
|
||||||
@ -284,38 +279,51 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
if parts.showQR {
|
if parts.showQR {
|
||||||
var text string
|
var text string
|
||||||
switch mode {
|
switch mode {
|
||||||
case "cert":
|
|
||||||
text = strings.TrimSpace(svc.PublicCert())
|
|
||||||
case "pub":
|
case "pub":
|
||||||
text = strings.TrimSpace(svc.PublicPEM())
|
text = strings.TrimSpace(svc.PublicPEM())
|
||||||
default:
|
default: // cert
|
||||||
text = strings.TrimSpace(svc.PublicPEM() + "\n" + svc.PublicCert())
|
text = strings.TrimSpace(svc.PublicCert())
|
||||||
}
|
}
|
||||||
makeQR(text, parts.pubQR)
|
makeQR(text, parts.pubQR)
|
||||||
// debug save for comparison
|
|
||||||
if parts.pubQR.Image != nil {
|
|
||||||
_ = os.MkdirAll("qr_debug", 0o755)
|
|
||||||
if f, e := os.Create(filepath.Join("qr_debug", "identity_current.png")); e == nil {
|
|
||||||
png.Encode(f, parts.pubQR.Image)
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
parts.pubQR.Image = nil
|
parts.pubQR.Image = nil
|
||||||
parts.pubQR.Refresh()
|
parts.pubQR.Refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
box := container.NewVBox(container.NewHBox(widget.NewLabel("QR obsah:"), chooser, layout.NewSpacer(), widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) })), parts.pubQR)
|
saveQR := widget.NewButtonWithIcon("Uložit QR", theme.DocumentSaveIcon(), func() {
|
||||||
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteBtn))
|
if parts.pubQR.Image == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||||
|
img := parts.pubQR.Image
|
||||||
|
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
||||||
|
if err != nil || wc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer wc.Close()
|
||||||
|
_ = png.Encode(wc, img)
|
||||||
|
parts.showToast("QR uložen")
|
||||||
|
}, win)
|
||||||
|
fd.SetFileName("identity_qr.png")
|
||||||
|
fd.Show()
|
||||||
|
})
|
||||||
|
qrRow := container.NewHBox(
|
||||||
|
widget.NewLabel("Obsah QR"),
|
||||||
|
chooser,
|
||||||
|
layout.NewSpacer(),
|
||||||
|
widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) }),
|
||||||
|
saveQR,
|
||||||
|
)
|
||||||
|
box := container.NewVBox(qrRow, container.NewCenter(parts.pubQR))
|
||||||
|
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteRow))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt tab
|
// Decrypt tab
|
||||||
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||||
parts.plainOut.Disable()
|
parts.plainOut.Disable()
|
||||||
parts.payload.Disable()
|
parts.plainOut.Wrapping = fyne.TextWrapWord
|
||||||
parts.payload.SetPlaceHolder("Cyphertext se načte po vložení QR kódu…")
|
parts.plainOut.SetMinRowsVisible(12)
|
||||||
parts.payload.OnChanged = nil
|
|
||||||
parts.payloadQR.FillMode = canvas.ImageFillContain
|
parts.payloadQR.FillMode = canvas.ImageFillContain
|
||||||
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
|
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
|
||||||
|
|
||||||
@ -339,7 +347,6 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPayload := func(cipher string, img image.Image) {
|
setPayload := func(cipher string, img image.Image) {
|
||||||
parts.payload.SetText(cipher)
|
|
||||||
parts.payloadQR.Image = img
|
parts.payloadQR.Image = img
|
||||||
parts.payloadQR.Refresh()
|
parts.payloadQR.Refresh()
|
||||||
decrypt(cipher)
|
decrypt(cipher)
|
||||||
@ -404,30 +411,26 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
fd.Show()
|
fd.Show()
|
||||||
})
|
})
|
||||||
|
|
||||||
copyPayloadBtn := widget.NewButtonWithIcon("Kopírovat payload", theme.ContentCopyIcon(), func() {
|
|
||||||
if parts.payload.Text == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
copyClip(parts.payload.Text, parts)
|
|
||||||
})
|
|
||||||
|
|
||||||
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
|
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
|
||||||
parts.payload.SetText("")
|
|
||||||
parts.payloadQR.Image = nil
|
parts.payloadQR.Image = nil
|
||||||
parts.payloadQR.Refresh()
|
parts.payloadQR.Refresh()
|
||||||
parts.plainOut.SetText("")
|
parts.plainOut.SetText("")
|
||||||
parts.showToast("Vymazáno")
|
parts.showToast("Vymazáno")
|
||||||
})
|
})
|
||||||
|
|
||||||
toolbar := container.NewHBox(pasteQRBtn, openQRBtn, copyPayloadBtn, clearBtn, layout.NewSpacer())
|
// Align buttons to the right by placing spacer first
|
||||||
|
toolbar := container.NewHBox(layout.NewSpacer(), pasteQRBtn, openQRBtn, clearBtn)
|
||||||
|
|
||||||
|
copyDecBtn := widget.NewButtonWithIcon("Kopírovat zprávu", theme.ContentCopyIcon(), func() {
|
||||||
|
if strings.TrimSpace(parts.plainOut.Text) != "" {
|
||||||
|
copyClip(parts.plainOut.Text, parts)
|
||||||
|
}
|
||||||
|
})
|
||||||
return container.NewVBox(
|
return container.NewVBox(
|
||||||
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
toolbar,
|
toolbar,
|
||||||
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
|
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
|
||||||
widget.NewLabel("Payload"),
|
container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), copyDecBtn),
|
||||||
parts.payload,
|
|
||||||
widget.NewLabel("Výsledek"),
|
|
||||||
parts.plainOut,
|
parts.plainOut,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -455,13 +458,11 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||||
lastCipher := ""
|
|
||||||
doEncrypt := func() {
|
doEncrypt := func() {
|
||||||
m := strings.TrimSpace(msgEntry.Text)
|
m := strings.TrimSpace(msgEntry.Text)
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
if m == "" {
|
if m == "" {
|
||||||
status.SetText("Zpráva je prázdná")
|
status.SetText("Zpráva je prázdná")
|
||||||
lastCipher = ""
|
|
||||||
updateQR("")
|
updateQR("")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -476,7 +477,7 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
|||||||
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
|
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fyne.Do(func() { lastCipher = res; updateQR(res); status.SetText("Hotovo") })
|
fyne.Do(func() { updateQR(res); status.SetText("Hotovo") })
|
||||||
}(m)
|
}(m)
|
||||||
}
|
}
|
||||||
var tmr *time.Timer
|
var tmr *time.Timer
|
||||||
@ -486,12 +487,6 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
|||||||
}
|
}
|
||||||
tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
|
tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
|
||||||
}
|
}
|
||||||
copyPayloadBtn := widget.NewButton("Kopírovat payload", func() {
|
|
||||||
if lastCipher != "" {
|
|
||||||
copyClip(lastCipher, parts)
|
|
||||||
status.SetText("Zkopírováno")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
|
copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
|
||||||
saveQRBtn := widget.NewButton("Uložit QR", func() {
|
saveQRBtn := widget.NewButton("Uložit QR", func() {
|
||||||
if qrImg.Image == nil {
|
if qrImg.Image == nil {
|
||||||
@ -509,7 +504,7 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
|||||||
fd.SetFileName("message_qr.png")
|
fd.SetFileName("message_qr.png")
|
||||||
fd.Show()
|
fd.Show()
|
||||||
})
|
})
|
||||||
content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyPayloadBtn, copyQRBtn, saveQRBtn), qrImg, status)
|
content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyQRBtn, saveQRBtn), qrImg, status)
|
||||||
title := ct.Name
|
title := ct.Name
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = "(bez názvu)"
|
title = "(bez názvu)"
|
||||||
@ -573,13 +568,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
if existing != nil {
|
if existing != nil {
|
||||||
certValue = existing.Cert
|
certValue = existing.Cert
|
||||||
}
|
}
|
||||||
manualEntry := widget.NewMultiLineEntry()
|
|
||||||
manualEntry.SetMinRowsVisible(4)
|
|
||||||
manualEntry.Wrapping = fyne.TextWrapWord
|
|
||||||
manualEntry.SetPlaceHolder("Sem lze vložit text PEM pokud QR selže…")
|
|
||||||
if certValue != "" {
|
|
||||||
manualEntry.SetText(certValue)
|
|
||||||
}
|
|
||||||
qrImg := canvas.NewImageFromImage(nil)
|
qrImg := canvas.NewImageFromImage(nil)
|
||||||
qrImg.FillMode = canvas.ImageFillContain
|
qrImg.FillMode = canvas.ImageFillContain
|
||||||
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
||||||
@ -597,23 +585,7 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateQR()
|
updateQR()
|
||||||
// now that updateQR exists, bind manualEntry changes
|
pasteQR := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
||||||
manualEntry.OnChanged = func(s string) {
|
|
||||||
certValue = s
|
|
||||||
updateQR()
|
|
||||||
}
|
|
||||||
pasteText := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
|
||||||
clip := fyne.CurrentApp().Clipboard().Content()
|
|
||||||
if strings.TrimSpace(clip) == "" {
|
|
||||||
parts.showToast("Schránka prázdná")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certValue = clip
|
|
||||||
manualEntry.SetText(certValue)
|
|
||||||
updateQR()
|
|
||||||
parts.showToast("Vloženo")
|
|
||||||
})
|
|
||||||
pasteQR := widget.NewToolbarAction(theme.ComputerIcon(), func() {
|
|
||||||
img, err := readImageClipboard()
|
img, err := readImageClipboard()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
parts.showToast("Chyba čtení schránky: " + err.Error())
|
parts.showToast("Chyba čtení schránky: " + err.Error())
|
||||||
@ -636,7 +608,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
||||||
certValue = txt2
|
certValue = txt2
|
||||||
manualEntry.SetText(certValue)
|
|
||||||
updateQR()
|
updateQR()
|
||||||
parts.showToast("Načteno z invert QR")
|
parts.showToast("Načteno z invert QR")
|
||||||
return
|
return
|
||||||
@ -654,7 +625,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
certValue = txt
|
certValue = txt
|
||||||
manualEntry.SetText(certValue)
|
|
||||||
updateQR()
|
updateQR()
|
||||||
parts.showToast("Načteno z QR")
|
parts.showToast("Načteno z QR")
|
||||||
})
|
})
|
||||||
@ -678,37 +648,14 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
certValue = txt
|
certValue = txt
|
||||||
manualEntry.SetText(certValue)
|
|
||||||
updateQR()
|
updateQR()
|
||||||
parts.showToast("Načteno z QR")
|
parts.showToast("Načteno z QR")
|
||||||
}, win)
|
}, win)
|
||||||
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
||||||
fd.Show()
|
fd.Show()
|
||||||
})
|
})
|
||||||
clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; manualEntry.SetText(""); updateQR() })
|
clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; updateQR() })
|
||||||
copyAct := widget.NewToolbarAction(theme.ContentCopyIcon(), func() {
|
toolbar := widget.NewToolbar(pasteQR, openImg, clearAct)
|
||||||
if certValue != "" {
|
|
||||||
copyClip(certValue, parts)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
saveAct := widget.NewToolbarAction(theme.DocumentSaveIcon(), func() {
|
|
||||||
if qrImg.Image == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
||||||
img := qrImg.Image
|
|
||||||
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
|
||||||
if err != nil || wc == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer wc.Close()
|
|
||||||
_ = png.Encode(wc, img)
|
|
||||||
parts.showToast("QR uložen")
|
|
||||||
}, win)
|
|
||||||
fd.SetFileName("contact_qr.png")
|
|
||||||
fd.Show()
|
|
||||||
})
|
|
||||||
toolbar := widget.NewToolbar(pasteText, pasteQR, openImg, widget.NewToolbarSeparator(), copyAct, saveAct, clearAct)
|
|
||||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||||
var popup dialog.Dialog
|
var popup dialog.Dialog
|
||||||
save := func(useEncrypt bool) {
|
save := func(useEncrypt bool) {
|
||||||
@ -744,12 +691,19 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ask {
|
if ask {
|
||||||
dialog.NewCustomConfirm("Common Name", "Použít", "Ponechat", widget.NewLabel(fmt.Sprintf("Common Name: %s\nPoužít jako název?", cn)), func(ok bool) {
|
entry := widget.NewEntry()
|
||||||
|
entry.SetText(name)
|
||||||
|
content := container.NewVBox(
|
||||||
|
widget.NewLabel(fmt.Sprintf("Common Name nalezen v certifikátu: %s", cn)),
|
||||||
|
widget.NewLabel("Chcete použít CN jako název, nebo jej upravit?"),
|
||||||
|
entry,
|
||||||
|
)
|
||||||
|
dialog.NewCustomConfirm("Název kontaktu", "Použít CN", "Uložit", content, func(ok bool) {
|
||||||
if ok {
|
if ok {
|
||||||
proceed(cn)
|
proceed(cn)
|
||||||
} else {
|
return
|
||||||
proceed(name)
|
|
||||||
}
|
}
|
||||||
|
proceed(strings.TrimSpace(entry.Text))
|
||||||
}, win).Show()
|
}, win).Show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -772,9 +726,8 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
parts.showToast("Smazáno")
|
parts.showToast("Smazáno")
|
||||||
}, win).Show()
|
}, win).Show()
|
||||||
})
|
})
|
||||||
useBtn := widget.NewButton("Použít ve Šifrování", func() { save(true) })
|
|
||||||
saveBtn := widget.NewButton("Uložit", func() { save(false) })
|
saveBtn := widget.NewButton("Uložit", func() { save(false) })
|
||||||
row := container.NewHBox(saveBtn, useBtn, layout.NewSpacer(), delBtn)
|
row := container.NewHBox(layout.NewSpacer(), saveBtn, delBtn, layout.NewSpacer())
|
||||||
title := "Nový kontakt"
|
title := "Nový kontakt"
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
title = "Upravit kontakt"
|
title = "Upravit kontakt"
|
||||||
@ -783,7 +736,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
|
popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
|
||||||
widget.NewLabel("Název"), nameEntry,
|
widget.NewLabel("Název"), nameEntry,
|
||||||
widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
|
widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
|
||||||
widget.NewLabel("Text PEM"), manualEntry,
|
|
||||||
widget.NewSeparator(), row), win)
|
widget.NewSeparator(), row), win)
|
||||||
popup.Resize(fyne.NewSize(640, 520))
|
popup.Resize(fyne.NewSize(640, 520))
|
||||||
popup.Show()
|
popup.Show()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user