diff --git a/.gitea/workflows/cross-compile.yaml b/.gitea/workflows/cross-compile.yaml new file mode 100644 index 0000000..e04e80e --- /dev/null +++ b/.gitea/workflows/cross-compile.yaml @@ -0,0 +1,109 @@ +name: Cross-Compile Binaries + +on: + workflow_dispatch: + push: + branches: + - '*' + schedule: + - cron: '0 0 * * 6' + +jobs: + compile-linux: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout and pull the code + uses: actions/checkout@v6 + + - name: Setup the Go programming language + uses: actions/setup-go@v6 + with: + go-version: 'stable' + + - name: Install build dependencies + run: | + mv /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/microsoft-prod.list.disabled + + until apt-get update; do + echo -e "-----\napt-get update failed, retrying in 1s...\n-----" + sleep 1s + done + + apt-get install -y \ + build-essential libgl1-mesa-dev libx11-dev libxrandr-dev \ + libxi-dev libxcursor-dev libxinerama-dev libglfw3-dev \ + libxxf86vm-dev + + - name: Compile the fyne application for native Linux + run: | + export CGO_ENABLED=1 + export CC=gcc + export CXX=g++ + export GOOS=linux + export GOARCH=amd64 + go build -o ./akartontv ./src + + - name: upload the building actifacts + uses: DaanSelen/upload-artifact-gitea@main + with: + name: package-linux64 + path: | + ./akartontv + retention-days: 3 + overwrite: true + + compile-windows: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout and pull the code + uses: actions/checkout@v6 + + - name: Setup the Go programming language + uses: actions/setup-go@v6 + with: + go-version: 'stable' + + - name: Install build dependencies + run: | + mv /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/microsoft-prod.list.disabled + + until apt-get update; do + echo -e "-----\napt-get update failed, retrying in 1s...\n-----" + sleep 1s + done + + apt-get install -y \ + build-essential libgl1-mesa-dev libx11-dev libxrandr-dev \ + libxi-dev libxcursor-dev libxinerama-dev libglfw3-dev \ + libxxf86vm-dev gcc-mingw-w64 gcc-multilib + + - name: Install go binary + run: | + go install github.com/tc-hib/go-winres@latest + env: + GOBIN: /usr/local/bin + + - name: Compile the fyne application for Windows + run: | + export CGO_ENABLED=1 + export GOOS=windows + export GOARCH=amd64 + export CC=x86_64-w64-mingw32-gcc + export CXX=x86_64-w64-mingw32-g++ + export CGO_LDFLAGS="-static-libgcc -static-libstdc++" + go-winres simply --icon icon.png --manifest gui + mv *.syso ./src + go build -o ./akartontv.exe -ldflags -H=windowsgui ./src + + - name: upload the building actifacts + uses: DaanSelen/upload-artifact-gitea@main + with: + name: package-win64 + path: | + ./akartontv.exe + retention-days: 3 + overwrite: true diff --git a/akartontv b/akartontv new file mode 100755 index 0000000..5fbd812 Binary files /dev/null and b/akartontv differ diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..0eb2ccc --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +go build -o akartontv src/*.go diff --git a/example.pptx b/example.pptx new file mode 100644 index 0000000..1a0f62b Binary files /dev/null and b/example.pptx differ diff --git a/go.mod b/go.mod index 9bbf526..8fe21cf 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( fyne.io/fyne/v2 v2.7.2 + github.com/pkg/sftp v1.13.10 golang.org/x/crypto v0.46.0 ) @@ -26,6 +27,7 @@ require ( github.com/hack-pad/safejs v0.1.1 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/kr/text v0.1.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect diff --git a/go.sum b/go.sum index 0287239..a1121ba 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wH github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -53,6 +55,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= diff --git a/install.sh b/install.sh index 22711eb..8baa2b3 100644 --- a/install.sh +++ b/install.sh @@ -14,13 +14,12 @@ systemctl enable --now ssh # Create the systemd userland folder if [[ ! -d /home/${userland_name}/.config/systemd/user ]]; then - mkdir -p /home/${userland_name}/.config/systemd/user -m 644 + mkdir -p /home/${userland_name}/.config/systemd/user -m 755 chown -R ${userland_name}:${userland_name} /home/${userland_name}/.config/systemd/user fi # Create the program place onto /opt if [[ ! -d /opt/${program_name} ]]; then - mkdir -p /opt/${program_name} -m 644 + mkdir -p /opt/${program_name} -m 755 chown -R ${userland_name}:${userland_name} /opt/${program_name} -fi - +fi \ No newline at end of file diff --git a/scholing.pptx b/scholing.pptx new file mode 100644 index 0000000..1e2b47f Binary files /dev/null and b/scholing.pptx differ diff --git a/src/action.go b/src/action.go index 186202a..df349c4 100644 --- a/src/action.go +++ b/src/action.go @@ -1,15 +1,21 @@ package main import ( + "io" "log" "net" + "os" "time" + "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) func translateRaspiName(targetName string) (string, string, string, string) { for _, raspiObj := range raspiList { + if targetName == "" { + break + } if raspiObj[0] == targetName { // Found! return raspiObj[1], raspiObj[2], raspiObj[3], raspiObj[4] @@ -19,7 +25,7 @@ func translateRaspiName(targetName string) (string, string, string, string) { return "", "", "", "" } -func verifyCred(targetName string) bool { +func createSSHClient(targetName string) (*ssh.Client, error) { hostname, port, username, password := translateRaspiName(targetName) log.Println("Connecting to:", hostname, port) @@ -29,18 +35,85 @@ func verifyCred(targetName string) bool { ssh.Password(password), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // OK for internal tools (and me in production) - Timeout: 5 * time.Second, + Timeout: 3 * time.Second, } connAddr := net.JoinHostPort(hostname, port) - client, err := ssh.Dial("tcp", connAddr, config) + return ssh.Dial("tcp", connAddr, config) +} + +func verifyCred(targetName string) bool { + sshClient, err := createSSHClient(targetName) if err != nil { log.Printf("Err occurred %v", err) return false } - defer client.Close() + defer sshClient.Close() log.Println("Success!") return true } + +func sftpUploadFile(targetName, localPath, remotePath string) bool { + sshClient, err := createSSHClient(targetName) + + if err != nil { + log.Printf("Failed to init the SSH connection %v", err) + return false + } + defer sshClient.Close() + + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + log.Printf("Failed to start SFTP: %v", err) + return false + } + defer sftpClient.Close() + + localFile, err := os.Open(localPath) + if err != nil { + log.Printf("Failed to open local file: %v", err) + return false + } + defer localFile.Close() + + remoteFile, err := sftpClient.Create(remotePath) + if err != nil { + log.Printf("Failed to open remote file handle: %v", err) + return false + } + + bytesCopied, err := io.Copy(remoteFile, localFile) + if err != nil { + log.Printf("Failed to copy local file to the remote location: %v", err) + return false + } + + log.Printf("Succesfully uploaded %d bytes to %s", bytesCopied, remotePath) + return true +} + +func restartShow(targetName string) bool { + sshClient, err := createSSHClient(targetName) + + if err != nil { + log.Printf("Failed to init the SSH connection: %v", err) + return false + } + defer sshClient.Close() + + session, err := sshClient.NewSession() + if err != nil { + log.Printf("Failed to create session over the connection: %v", err) + } + defer session.Close() + + output, err := session.CombinedOutput("systemctl --user restart pres") + if err != nil { + log.Printf("Failed to restart the show over SSH: %v", err) + } + + log.Println(output) + return true +} diff --git a/src/draw.go b/src/draw.go index 2f86f9d..c537f47 100644 --- a/src/draw.go +++ b/src/draw.go @@ -10,18 +10,34 @@ import ( "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" ) -func flashRed(btnPtr *widget.Button) { +var ( + presFileFilter []string = []string{".pptx", ".odp"} + videoFileFilter []string = []string{".mp4", ".mkv", ".mov", ".webm"} +) + +func flashColor(btnPtr *widget.Button, color string) { + var sleepAmount int = 1 + // "red" or "green" // Set red on UI thread + fyne.CurrentApp().Driver().DoFromGoroutine(func() { - btnPtr.Importance = widget.DangerImportance + switch color { + case "red": + btnPtr.Importance = widget.DangerImportance + case "green": + btnPtr.Importance = widget.SuccessImportance + sleepAmount = 3 + } refreshButtons(btnPtr) }, false) - log.Println("Waiting") - time.Sleep(1 * time.Second) + log.Println("Waiting to change button") + time.Sleep(time.Duration(sleepAmount) * time.Second) + log.Println("Done waiting") // Reset on UI thread fyne.CurrentApp().Driver().DoFromGoroutine(func() { @@ -30,10 +46,6 @@ func flashRed(btnPtr *widget.Button) { }, false) } -func drawVerifyProcess() { - -} - func drawSeparator(trailNewLine bool) *fyne.Container { emptyLabel := widget.NewLabel("") // Give it some space at the bottom with an empty line separatorLine := canvas.NewLine(color.Gray{Y: 128}) @@ -55,7 +67,7 @@ func drawSeparator(trailNewLine bool) *fyne.Container { return separContainer } -func drawButtonRow(targetMode *int) *fyne.Container { +func drawModeRow(targetMode *int) *fyne.Container { actionText := widget.NewLabel("Select Mode") // targetMode is defined as: // 0 undefined / not used @@ -101,7 +113,7 @@ func drawButtonRow(targetMode *int) *fyne.Container { return modeCol } -func drawTargetSection(raspiNames []string, raspiTarget *string) *fyne.Container { +func drawTargetSection(raspiNames []string, raspiTarget *string, uploadBtn, reloadBtn *widget.Button) *fyne.Container { actionText := widget.NewLabel("Select Target") var verifyBtn *widget.Button @@ -112,9 +124,13 @@ func drawTargetSection(raspiNames []string, raspiTarget *string) *fyne.Container log.Println("Selected target:", selected) } else { log.Println("Deselected Target") + *raspiTarget = "" } + // Reload all buttons! verifyBtn.Importance = widget.LowImportance - refreshButtons(verifyBtn) + uploadBtn.Importance = widget.LowImportance + reloadBtn.Importance = widget.LowImportance + refreshButtons(verifyBtn, uploadBtn, reloadBtn) }) selecCol := container.NewVBox( @@ -128,17 +144,23 @@ func drawTargetSection(raspiNames []string, raspiTarget *string) *fyne.Container refreshButtons(verifyBtn) log.Println("Verifying credentials...") + uploadBtn.Disable() + reloadBtn.Disable() + refreshButtons(verifyBtn, uploadBtn, reloadBtn) + go func() { credOK = verifyCred(*raspiTarget) if credOK { // Must update UI on main thread fyne.CurrentApp().Driver().DoFromGoroutine(func() { - verifyBtn.Importance = widget.SuccessImportance - refreshButtons(verifyBtn) - }, true) + go flashColor(verifyBtn, "green") // flashcolor should handle RunOnMain internally + uploadBtn.Enable() + reloadBtn.Enable() + }, false) + } else { - flashRed(verifyBtn) // flashRed should handle RunOnMain internally + go flashColor(verifyBtn, "red") // flashcolor should handle RunOnMain internally } }() }) @@ -156,7 +178,7 @@ func drawTargetSection(raspiNames []string, raspiTarget *string) *fyne.Container return wholeCol } -func drawFileSelection(localPath *string, parentWindow *fyne.Window) *fyne.Container { +func drawFileSelection(localPath *string, targetMode *int, parentWindow *fyne.Window) *fyne.Container { actionText := widget.NewLabel("Select File") pathLabel := widget.NewLabel("No File Selected Yet...") @@ -165,7 +187,7 @@ func drawFileSelection(localPath *string, parentWindow *fyne.Window) *fyne.Conta log.Println("Ignore the next error. ~Refer: https://github.com/fyne-io/fyne/issues/4110") // Use NewFileOpen to get the dialog object - uploadDiag := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { + uploadSelecDiag := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if r != nil { defer r.Close() @@ -176,12 +198,19 @@ func drawFileSelection(localPath *string, parentWindow *fyne.Window) *fyne.Conta } }, *parentWindow) - //fileFilter := storage.NewExtensionFileFilter() // <- needs a list[str] of extensions if we want to filter - //uploadDiag.SetFilter(fileFilter) + var fileFilter storage.FileFilter + switch *targetMode { + case 1: + fileFilter = storage.NewExtensionFileFilter(presFileFilter) + case 2: + fileFilter = storage.NewExtensionFileFilter(videoFileFilter) + } + + uploadSelecDiag.SetFilter(fileFilter) // Resize the dialog to the whole window, if it wants to do that - - uploadDiag.Resize(windowSize) - uploadDiag.Show() + uploadSelecDiag.Resize(windowSize) + uploadSelecDiag.Show() }) uploadWide := container.NewGridWrap( buttonSize, @@ -201,14 +230,42 @@ func drawFileSelection(localPath *string, parentWindow *fyne.Window) *fyne.Conta return fileSelecCol } -func drawFooter(app fyne.App, targetMode *int) *fyne.Container { +// targetMode *int +func drawFooter(app fyne.App, raspiTarget, localUploadPath *string) (*fyne.Container, *widget.Button, *widget.Button) { + remotePresPath := "/opt/akartontv/presentation.pptx" + //remoteVideoPath := "/opt/akartontv/video.mp4" + // Configuration of the bottom of the application - showBtn := widget.NewButton("Print Current Value", func() { - log.Println(*targetMode) + var uploadBtn *widget.Button + uploadBtn = widget.NewButton("Upload File", func() { + uploadBtn.Importance = widget.HighImportance + refreshButtons(uploadBtn) + + if sftpUploadFile(*raspiTarget, *localUploadPath, remotePresPath) { + go flashColor(uploadBtn, "green") + } else { + go flashColor(uploadBtn, "red") + } }) - showWide := container.NewGridWrap( + uploadWide := container.NewGridWrap( buttonSize, - showBtn, + uploadBtn, + ) + + var reloadBtn *widget.Button + reloadBtn = widget.NewButton("Restart Program", func() { + reloadBtn.Importance = widget.HighImportance + refreshButtons(uploadBtn) + + if restartShow(*raspiTarget) { + go flashColor(reloadBtn, "green") // flashcolor should handle RunOnMain internally + } else { + go flashColor(reloadBtn, "red") + } + }) + reloadWide := container.NewGridWrap( + buttonSize, + reloadBtn, ) cancelBtn := widget.NewButton("Exit", func() { @@ -221,10 +278,13 @@ func drawFooter(app fyne.App, targetMode *int) *fyne.Container { ) bottom := container.NewHBox( - showWide, // left + uploadWide, // left + reloadWide, // also next to the one above layout.NewSpacer(), // flexible space cancelWide, // right ) - return bottom + uploadBtn.Disable() + reloadBtn.Disable() + return bottom, uploadBtn, reloadBtn } diff --git a/src/main.go b/src/main.go index 597888b..d6084b3 100644 --- a/src/main.go +++ b/src/main.go @@ -22,9 +22,8 @@ const ( ) var ( - windowSize fyne.Size = fyne.NewSize(750, 800) // Default Window size - //dialogSize fyne.Size = fyne.NewSize(750, 700) // For some reason when we use windowSize for that, its still smaller, this is also smaller. But a better fit - buttonSize fyne.Size = fyne.NewSize(200, 50) // Default button size + windowSize fyne.Size = fyne.NewSize(750, 700) // Default Window size + buttonSize fyne.Size = fyne.NewSize(200, 50) // Default button size entrySize fyne.Size = fyne.NewSize(200, 40) ) @@ -38,24 +37,32 @@ func main() { raspiNames := getRaspiNames() // Define variables and print them out for debug + // Presentation = 1 + // Video = 2 var targetMode int = 0 - var raspiTarget string + var raspiTarget string = "" var localUploadPath string + log.Println("Current mode:", targetMode) - log.Println("Current target:", raspiTarget) + if raspiTarget == "" { + log.Println("Current target: None") + } else { + log.Println("Current target:", raspiTarget) + } // Call the draw functions -> ./src/draw.go - modeBtnRow := drawButtonRow(&targetMode) - drawSelecRow := drawTargetSection(raspiNames, &raspiTarget) - fileSelectRow := drawFileSelection(&localUploadPath, &w) - footerRow := drawFooter(app, &targetMode) + footerRow, uploadBtn, reloadBtn := drawFooter(app, &raspiTarget, &localUploadPath) + modeBtnRow := drawModeRow(&targetMode) + selectionRow := drawTargetSection(raspiNames, &raspiTarget, uploadBtn, reloadBtn) + fileSelectRow := drawFileSelection(&localUploadPath, &targetMode, &w) center := container.NewVBox( modeBtnRow, - drawSeparator(true), - drawSelecRow, - drawSeparator(false), + widget.NewLabel(""), fileSelectRow, + drawSeparator(true), + selectionRow, + drawSeparator(false), ) content := container.NewBorder( @@ -64,7 +71,6 @@ func main() { nil, nil, // left, right center, //center ) - log.Println(raspiTarget) w.SetContent(content) w.ShowAndRun() @@ -72,7 +78,9 @@ func main() { func refreshButtons(givenButtons ...*widget.Button) { for _, btn := range givenButtons { - btn.Refresh() + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + btn.Refresh() + }, false) } } diff --git a/src/rpi-list.go b/src/rpi-list.go index 3afbfe7..26f2146 100644 --- a/src/rpi-list.go +++ b/src/rpi-list.go @@ -10,6 +10,6 @@ var ( raspiList [][]string = [][]string{ {"Kantine Pi", "172.16.64.228", "22", "systemec", ""}, {"Productie Pi", "172.16.64.229", "22", "", ""}, - {"Sales Pi", "", "", "", ""}, + {"Sales Pi", "192.168.110.175", "22", "systemec", "Doerak2003!"}, } )