From 3e77a26b0a42aadcbec47f45f0917e6cf41ebdd3 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 26 Nov 2025 16:28:37 +0300 Subject: [PATCH] feat: gtk --- Makefile | 3 +- cmd/main.go | 3 +- go.mod | 2 +- go.sum | 2 + internal/constants/app.go | 1 + internal/ui/builder.go | 36 +++ internal/ui/handlers.go | 7 - internal/ui/mainwindow.go | 74 ++++++ internal/ui/resources/qrcode.png | Bin 0 -> 6428 bytes internal/ui/resources/resources.go | 30 +++ internal/ui/resources/window.glade | 367 +++++++++++++++++++++++++++++ internal/ui/window.go | 1 - 12 files changed, 514 insertions(+), 12 deletions(-) create mode 100644 internal/ui/builder.go delete mode 100644 internal/ui/handlers.go create mode 100644 internal/ui/mainwindow.go create mode 100644 internal/ui/resources/qrcode.png create mode 100644 internal/ui/resources/resources.go create mode 100644 internal/ui/resources/window.glade delete mode 100644 internal/ui/window.go diff --git a/Makefile b/Makefile index 40bc3fb..a791eda 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BUILD_DIR := bin CMD_PATH := ./cmd GOCMD := go -GOBUILD := $(GOCMD) build +GOBUILD := $(GOCMD) build -v GOTEST := $(GOCMD) test GOCLEAN := $(GOCMD) clean GOTIDY := $(GOCMD) mod tidy @@ -42,6 +42,7 @@ tidy: clean: @$(RM) $(BINARY_NAME) + @go clean -modcache @echo "Clean complete" help: diff --git a/cmd/main.go b/cmd/main.go index 75773bb..9452dd5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,8 +37,7 @@ func main() { screencapturer.NewWholeScreenCapturer, vision.NewVision, ), - fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) { - log.Debug("starting application...") + fx.Invoke(func() { }), ) diff --git a/go.mod b/go.mod index f130c11..abfe5ff 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/gen2brain/shm v0.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gotk3/gotk3 v0.6.4 + github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 github.com/jezek/xgb v1.1.1 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 5179238..a03fcb7 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 h1:BelWQzAzJfSMA1qbuzoV9Tp57+NCvYouEA5cWVpYcSk= +github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= diff --git a/internal/constants/app.go b/internal/constants/app.go index 4e6eb7e..b281f50 100644 --- a/internal/constants/app.go +++ b/internal/constants/app.go @@ -21,4 +21,5 @@ package constants const ( AppName = "auto-attendance" + AppClassGtk = "su.weirdcat.autoattendance" ) diff --git a/internal/ui/builder.go b/internal/ui/builder.go new file mode 100644 index 0000000..6ae52af --- /dev/null +++ b/internal/ui/builder.go @@ -0,0 +1,36 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "github.com/gotk3/gotk3/gtk" +) + +func NewBuilder() (*gtk.Builder, error) { + + gtk.Init(nil) + + builder, err := gtk.BuilderNew() + if err != nil { + return nil, err + } + + return builder, nil +} diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go deleted file mode 100644 index 91836b7..0000000 --- a/internal/ui/handlers.go +++ /dev/null @@ -1,7 +0,0 @@ -package ui - -import "github.com/gotk3/gotk3/gtk" - -type Handlers struct { - gtk.AboutDialog -} diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go new file mode 100644 index 0000000..a8684b0 --- /dev/null +++ b/internal/ui/mainwindow.go @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "git.weirdcat.su/weirdcat/auto-attendance/internal/ui/resources" + "github.com/gotk3/gotk3/gtk" +) + +type MainWindow struct { + builder *gtk.Builder + window *gtk.Window +} + +func (m *MainWindow) Start() { + m.window.ShowAll() + go func() { gtk.Main() }() +} + +func NewMainWindow(builder *gtk.Builder) (*MainWindow, error) { + err := builder.AddFromString(string(resources.GladeMainWindow)) + if err != nil { + return nil, err + } + + windowObj, err := builder.GetObject("window_main") + if err != nil { + return nil, err + } + window := windowObj.(*gtk.Window) + window.Connect("destroy", func () { + gtk.MainQuit() + }) + + notebookObj, err := builder.GetObject("notebook_main") + if err != nil { + return nil, err + } + notebook := notebookObj.(*gtk.Notebook) + + mainPageObj, err := builder.GetObject("page_main") + if err != nil { + return nil, err + } + mainPage := mainPageObj.(*gtk.Grid) + + settingsPageObj, err := builder.GetObject("page_settings") + if err != nil { + return nil, err + } + settingsPage := settingsPageObj.(*gtk.Box) + + notebook.SetTabLabelText(mainPage, "Overview") + notebook.SetTabLabelText(settingsPage, "Settings") + + return &MainWindow{builder: builder, window: window}, nil +} diff --git a/internal/ui/resources/qrcode.png b/internal/ui/resources/qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..baeb4f1a96be05ed3c0ffc35a212a8e79aa36d41 GIT binary patch literal 6428 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. + +package resources + +import _ "embed" + +var ( + //go:embed window.glade + GladeMainWindow []byte + + //go:embed qrcode.png + QrCodePng []byte +) diff --git a/internal/ui/resources/window.glade b/internal/ui/resources/window.glade new file mode 100644 index 0000000..e50eaf3 --- /dev/null +++ b/internal/ui/resources/window.glade @@ -0,0 +1,367 @@ + + + + + + 100 + 5000 + 500 + 100 + 500 + + + [ERROR] 2025-11-26 14:24:15 - Failed to fetch data from API: Timeout occurred after 30 seconds. +[ERROR] 2025-11-26 14:24:30 - Exception: NullReferenceException in ModuleX: Object reference not set to an instance of an object. + + + + window_main + False + 400 + 600 + zoom-best-fit + + + True + True + + + + App + True + False + True + True + + + True + True + bottom-left + in + + + True + True + 10 + 10 + 10 + 10 + False + word-char + console_output + terminal + True + + + + + 0 + 3 + + + + + + True + False + True + True + + + True + False + vertical + top + + + + + + True + False + vertical + + + + + + False + True + 1 + + + + + + + + 0 + 0 + + + + + 0 + 0 + 3 + + + + + True + False + + + + + Settings + True + False + 20 + 20 + 20 + 20 + vertical + 12 + + + True + False + 0 + none + + + True + False + 12 + 12 + 12 + 12 + vertical + 6 + + + Auto-scan for QR codes + True + True + False + True + True + + + False + True + 0 + + + + + Beep on successful scan + True + True + False + True + True + + + False + True + 1 + + + + + + + True + False + Scanning Options + + + + + + + + False + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + 12 + 12 + 12 + vertical + 6 + + + True + False + 6 + + + True + False + Scan interval (ms): + 0 + + + False + True + 0 + + + + + True + True + adjustment1 + 1 + True + True + 500 + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 6 + + + True + False + Output format: + 0 + + + False + True + 0 + + + + + True + False + 0 + + Text + JSON + XML + URL + + + + False + True + 1 + + + + + False + True + 1 + + + + + + + True + False + Advanced Settings + + + + + + + + False + True + 1 + + + + + Save Settings + True + True + True + end + 12 + + + False + True + 2 + + + + + 1 + True + + + + + + + True + False + Qrminator + Offline + 1 + True + + + True + True + Toggle QR-searching + + + Toggle QR-handling + When enabled, program will search QR-codes on the screen and react to them according to user settings + + + + + + + True + False + True + + + 1 + + + + + + diff --git a/internal/ui/window.go b/internal/ui/window.go deleted file mode 100644 index 5b1faa2..0000000 --- a/internal/ui/window.go +++ /dev/null @@ -1 +0,0 @@ -package ui