commit 5f6d475083d10dafa8fe7c08bb150848264ba196 Author: Different55 Date: Sat Jun 22 19:59:57 2024 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08cb523 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a77d6ad --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/different55/deckspin + +go 1.20 + +require ( + github.com/saracen/fastzip v0.1.11 + github.com/webview/webview_go v0.0.0-20240220051247-56f456ca3a43 +) + +require ( + github.com/klauspost/compress v1.16.5 // indirect + github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..5f0c530 --- /dev/null +++ b/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "github.com/saracen/fastzip" + webview "github.com/webview/webview_go" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +//go:embed main.js +var initScript string + +func main() { + w := webview.New(true) + defer w.Destroy() + w.SetTitle("DeckSpin") + w.SetSize(1280, 800, webview.HintNone) + w.Navigate("https://spinsha.re") + w.Bind("log", fmt.Println) + w.Bind("installChart", InstallChart) + w.Init(initScript) + w.Run() +} + +func InstallChart(url string) { + println("Installing chart") + + // Make sure the URL is a SpinShare URL + if !strings.HasPrefix(url, "https://spinsha.re/") { + println("Not a SpinShare URL") + return + } + + // Make sure it's a chart download URL + if !strings.HasSuffix(url, "/download") { + println("Not a download URL") + return + } + + // Create a temporary file. + tmp, err := os.CreateTemp("/tmp/", "deckspin") + if err != nil { + println("Failed to create temporary file") + return + } + defer tmp.Close() + + // Download the ZIP file + resp, err := http.Get(url) + if err != nil { + println("Failed to download ZIP file") + return + } + defer resp.Body.Close() + + // Write the ZIP file to the temporary file + _, err = io.Copy(tmp, resp.Body) + if err != nil { + println("Failed to write ZIP file") + return + } + + tmpInfo, err := tmp.Stat() + if err != nil { + print("Failed to get temporary file info") + return + } + + // Get user home directory. + home, err := os.UserHomeDir() + if err != nil { + println("Failed to get user home directory") + return + } + + // Unzip the ZIP file + e, err := fastzip.NewExtractorFromReader(tmp, tmpInfo.Size(), filepath.Join(home, ".steam/steam/steamapps/compatdata/1058830/pfx/drive_c/users/steamuser/AppData/LocalLow/Super Spin Digital/Spin Rhythm XD/Custom/")) + if err != nil { + print("Failed to create extractor") + return + } + + if err = e.Extract(context.Background()); err != nil { + print("Failed to extract ZIP file") + return + } + + println("Chart installed") +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..5e2c79c --- /dev/null +++ b/main.js @@ -0,0 +1,172 @@ +window.log("loaded " + window.location.href ); + +document.addEventListener("DOMContentLoaded", function() { + // Add an onclick listener to all anchors, buttons, and inputs to check whether + // this is a download link and if so, install the chart. + let controls = document.querySelectorAll('a:not(.button-disabled):not([disabled]), button:not([disabled]), input:not([disabled])'); + controls.forEach(function(control) { + control.addEventListener("click", function() { + let href = control.href; + if (href && href.endsWith("/download")) { + window.log("Installing chart: " + href); + document.body.classList.add("ds-download"); + window.setTimeout(function() { + window.installChart(href); + document.body.classList.remove("ds-download"); + }, 100); + } + }); + }); + + // Add a new stylesheet to the document to style the active control. + let style = document.createElement("style"); + style.innerHTML = ` + .ds-active { + position: relative; + } + .ds-active::after { + content: ""; + border: 2px solid #e22c78; + outline: none; + + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + pointer-events: none; + } + .ds-download { + position: relative; + } + .ds-download::after { + content: "Downloading..."; + font-size: 2em; + color: #e22c78; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + font-weight: 200; + } + .ds-download main { + opacity: 0.25; + } + `; + document.head.appendChild(style); +}); + +// Enable arrow keys for navigation. +document.onkeydown = function(e) { + switch (e.code) { + case "ArrowUp": + e.preventDefault(); + getControlsInDirection("up"); + break; + case "ArrowDown": + e.preventDefault(); + getControlsInDirection("down"); + break; + case "ArrowLeft": + e.preventDefault(); + getControlsInDirection("left"); + break; + case "ArrowRight": + e.preventDefault(); + getControlsInDirection("right"); + break; + } +} + +function getControlsInDirection(direction) { + let controls = document.querySelectorAll('a:not(.button-disabled):not([disabled]), button:not([disabled]), input:not([disabled])'); + if (!controls.length) + return; + + let activeControl = document.querySelector(".ds-active"); + if (!activeControl) { + controls[0].classList.add("ds-active"); + controls[0].focus(); + return; + } + + let activeRect = activeControl.getBoundingClientRect(); + let activeCenter = 0; + + switch (direction) { + case "up": + case "down": + activeCenter = activeRect.top+activeRect.height/2; + break; + case "left": + case "right": + activeCenter = activeRect.left+activeRect.width/2; + break; + default: + return; + } + + let closestDistance = Number.MAX_VALUE; + let closestControl = null; + + let skipOverlapCheck = false; + + function getClosest(control) { + if (control === activeControl) + return; + + let rect = control.getBoundingClientRect(); + let center = 0; + let distance; + + switch (direction) { + case "up": + case "down": + center = rect.top + rect.height / 2; + break; + case "left": + case "right": + center = rect.left + rect.width / 2; + break; + } + + if (direction === "up" && (skipOverlapCheck || (activeRect.left < rect.right && activeRect.right > rect.left))) + distance = activeCenter - center; + else if (direction === "down" && (skipOverlapCheck || (activeRect.left < rect.right && activeRect.right > rect.left))) + distance = center - activeCenter; + else if (direction === "left" && (skipOverlapCheck || (activeRect.top < rect.bottom && activeRect.bottom > rect.top))) + distance = activeCenter - center; + else if (direction === "right" && (skipOverlapCheck || (activeRect.top < rect.bottom && activeRect.bottom > rect.top))) + distance = center - activeCenter; + else + return; + + if (distance <= 0) + return; + + if (distance < closestDistance) { + closestDistance = distance; + closestControl = control; + } + } + + controls.forEach(getClosest); + + if (closestControl) { + activeControl.classList.remove("ds-active"); + closestControl.classList.add("ds-active"); + closestControl.focus(); + return; + } + + // If no control was found, try again without checking for overlap. + skipOverlapCheck = true; + controls.forEach(getClosest); + + if (closestControl) { + activeControl.classList.remove("ds-active"); + closestControl.classList.add("ds-active"); + closestControl.focus(); + } +} \ No newline at end of file