Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
bbc4fd4c32
|
|||
|
06f3e6fa5a
|
|||
|
548d5dbc35
|
|||
|
78c07e1c6b
|
|||
|
4afe001117
|
|||
|
826e7d10d1
|
|||
|
4646423f1d
|
|||
|
a3a8e29a6d
|
|||
|
a2411bd176
|
|||
|
2465e2e42b
|
|||
|
3d81697538
|
|||
|
5896cd7064
|
|||
|
9ffce1e12f
|
|||
|
b4c65bf30b
|
|||
|
7d2eb81e0f
|
|||
|
837c5c471d
|
|||
|
4b9a972706
|
@@ -1,14 +1,32 @@
|
||||
node_modules
|
||||
# Dependencies
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
# SvelteKit & Vite
|
||||
.svelte-kit/
|
||||
build/
|
||||
dist/
|
||||
.output/
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# OS
|
||||
# Go & Binaries
|
||||
bin/
|
||||
linking-tool
|
||||
tmp/
|
||||
|
||||
# Wails Desktop
|
||||
desktop/frontend_dist/
|
||||
desktop/build/
|
||||
wailsjs/
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE & OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -16,8 +34,3 @@ Thumbs.db
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
@@ -7,33 +7,44 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Svelte check (fail on warnings)
|
||||
- name: Frontend checks
|
||||
run: bash scripts/check.sh
|
||||
- name: Build frontend
|
||||
run: bash scripts/build.sh
|
||||
- name: Upload frontend assets
|
||||
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: build/
|
||||
|
||||
build:
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check
|
||||
needs: build-frontend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Download frontend assets
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: bash scripts/build.sh
|
||||
name: frontend-build
|
||||
path: build/
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
- name: Build backend
|
||||
run: |
|
||||
mkdir -p bin
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/linking-tool main.go
|
||||
|
||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,23 +1,35 @@
|
||||
node_modules
|
||||
# Dependencies
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
# SvelteKit & Vite
|
||||
.svelte-kit/
|
||||
build/
|
||||
dist/
|
||||
.output/
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# OS
|
||||
# Go & Binaries
|
||||
bin/
|
||||
linking-tool
|
||||
tmp/
|
||||
|
||||
# Wails Desktop
|
||||
desktop/frontend_dist/
|
||||
desktop/build/bin/
|
||||
desktop/build/
|
||||
wailsjs/
|
||||
|
||||
# IDE & OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
39
Dockerfile
39
Dockerfile
@@ -1,33 +1,30 @@
|
||||
FROM cgr.dev/chainguard/node:latest-dev AS builder
|
||||
|
||||
# Stage 1: Build the frontend
|
||||
FROM cgr.dev/chainguard/node:latest-dev AS node-builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
RUN npm install --save-dev @sveltejs/adapter-node@latest
|
||||
|
||||
COPY --chown=node:node . .
|
||||
COPY --chown=node:node svelte.config.docker.js svelte.config.js
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM cgr.dev/chainguard/node:latest AS runtime
|
||||
|
||||
# Stage 2: Build the Go binary with embedded assets
|
||||
FROM cgr.dev/chainguard/go:latest-dev AS go-builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=node-builder /app/build ./build
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o linking-tool main.go
|
||||
|
||||
COPY --from=builder --chown=node:node /app/package.json /app/package-lock.json ./
|
||||
RUN npm install --omit=dev && \
|
||||
npm cache clean --force
|
||||
|
||||
COPY --from=builder --chown=node:node /app/build ./build
|
||||
COPY --from=builder --chown=node:node /app/package.json ./
|
||||
|
||||
EXPOSE 3000
|
||||
# Stage 3: Minimal runtime image
|
||||
FROM cgr.dev/chainguard/wolfi-base:latest
|
||||
WORKDIR /app
|
||||
COPY --from=go-builder /app/linking-tool .
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
EXPOSE 8080
|
||||
ENV PORT=8080
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
CMD ["build/index.js"]
|
||||
USER 65532
|
||||
|
||||
CMD ["./linking-tool"]
|
||||
|
||||
41
Dockerfile.build
Normal file
41
Dockerfile.build
Normal file
@@ -0,0 +1,41 @@
|
||||
FROM cgr.dev/chainguard/node:latest-dev AS node-builder
|
||||
WORKDIR /app
|
||||
COPY --chown=node:node package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY --chown=node:node . .
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
# Install dependencies for Wails on Alpine
|
||||
# Added webkit2gtk-4.1-dev which is the modern package name in Alpine
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
make \
|
||||
gcc \
|
||||
musl-dev \
|
||||
pkgconfig \
|
||||
gtk+3.0-dev \
|
||||
webkit2gtk-4.1-dev \
|
||||
curl
|
||||
|
||||
# Install Wails
|
||||
RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
ENV PATH=$PATH:/root/go/bin
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=node-builder /app/build ./build
|
||||
|
||||
# Build the Go server
|
||||
RUN mkdir -p bin && \
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/linking-tool main.go
|
||||
|
||||
# Build desktop apps
|
||||
RUN mkdir -p desktop/frontend_dist && \
|
||||
cp -r build/* desktop/frontend_dist/ && \
|
||||
cd desktop && wails build -s -platform linux/amd64 -o linking-tool-linux
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /app/bin /bin
|
||||
COPY --from=builder /app/desktop/build/bin /desktop-bin
|
||||
86
Makefile
86
Makefile
@@ -1,44 +1,92 @@
|
||||
.PHONY: help install dev build preview check lint format clean docker-build docker-run docker package publish
|
||||
BINARY_NAME=linking-tool
|
||||
BUILD_DIR=bin
|
||||
|
||||
.PHONY: help install dev build preview check lint format clean docker-build docker-run docker-builder release build-linux-amd64 build-linux-arm64 build-linux-armv6 build-linux-armv7 build-windows-amd64 build-darwin-amd64 build-darwin-arm64 build-freebsd-amd64 desktop-build desktop-windows desktop-darwin desktop-dev
|
||||
|
||||
help:
|
||||
@echo 'Usage: make [target]'
|
||||
@echo ''
|
||||
@echo 'Available targets:'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
dev:
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
build:
|
||||
npm install
|
||||
npm run build
|
||||
mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o $(BUILD_DIR)/$(BINARY_NAME) main.go
|
||||
|
||||
package:
|
||||
npm run package
|
||||
release: build
|
||||
mkdir -p $(BUILD_DIR)
|
||||
@$(MAKE) build-linux-amd64
|
||||
@$(MAKE) build-linux-arm64
|
||||
@$(MAKE) build-linux-armv6
|
||||
@$(MAKE) build-linux-armv7
|
||||
@$(MAKE) build-windows-amd64
|
||||
@$(MAKE) build-darwin-amd64
|
||||
@$(MAKE) build-darwin-arm64
|
||||
@$(MAKE) build-freebsd-amd64
|
||||
|
||||
publish:
|
||||
npm publish
|
||||
build-linux-amd64:
|
||||
GOOS=linux GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 main.go
|
||||
|
||||
preview:
|
||||
npm run preview
|
||||
build-linux-arm64:
|
||||
GOOS=linux GOARCH=arm64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 main.go
|
||||
|
||||
check:
|
||||
npm run check
|
||||
build-linux-armv6:
|
||||
GOOS=linux GOARCH=arm GOARM=6 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux-armv6 main.go
|
||||
|
||||
lint:
|
||||
npm run lint
|
||||
build-linux-armv7:
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux-armv7 main.go
|
||||
|
||||
format:
|
||||
npm run format
|
||||
build-windows-amd64:
|
||||
GOOS=windows GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe main.go
|
||||
|
||||
clean:
|
||||
rm -rf .svelte-kit build node_modules/.vite dist package
|
||||
build-darwin-amd64:
|
||||
GOOS=darwin GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 main.go
|
||||
|
||||
build-darwin-arm64:
|
||||
GOOS=darwin GOARCH=arm64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 main.go
|
||||
|
||||
build-freebsd-amd64:
|
||||
GOOS=freebsd GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-freebsd-amd64 main.go
|
||||
|
||||
docker-build:
|
||||
docker build -t linking-tool .
|
||||
docker build -t $(BINARY_NAME) .
|
||||
|
||||
docker-run:
|
||||
docker run --rm -p 3000:3000 linking-tool
|
||||
docker run -p 8080:8080 $(BINARY_NAME)
|
||||
|
||||
docker: docker-build docker-run
|
||||
docker-builder:
|
||||
docker build -f Dockerfile.build -t $(BINARY_NAME)-build .
|
||||
docker create --name $(BINARY_NAME)-temp $(BINARY_NAME)-build
|
||||
mkdir -p $(BUILD_DIR)
|
||||
docker cp $(BINARY_NAME)-temp:/bin/. $(BUILD_DIR)/
|
||||
docker cp $(BINARY_NAME)-temp:/desktop-bin/. $(BUILD_DIR)/
|
||||
docker rm $(BINARY_NAME)-temp
|
||||
|
||||
desktop-build: build
|
||||
rm -rf desktop/frontend_dist/*
|
||||
cp -r build/* desktop/frontend_dist/
|
||||
cd desktop && wails build -s
|
||||
|
||||
desktop-windows: build
|
||||
rm -rf desktop/frontend_dist/*
|
||||
cp -r build/* desktop/frontend_dist/
|
||||
cd desktop && wails build -s -platform windows/amd64
|
||||
|
||||
desktop-darwin: build
|
||||
rm -rf desktop/frontend_dist/*
|
||||
cp -r build/* desktop/frontend_dist/
|
||||
cd desktop && wails build -s -platform darwin/universal
|
||||
|
||||
desktop-dev: build
|
||||
rm -rf desktop/frontend_dist/*
|
||||
cp -r build/* desktop/frontend_dist/
|
||||
cd desktop && wails dev
|
||||
|
||||
clean:
|
||||
rm -rf .svelte-kit build node_modules/.vite dist package linking-tool tmp $(BUILD_DIR)
|
||||
|
||||
57
README.md
57
README.md
@@ -13,11 +13,20 @@ A client-side web linking tool for mapping relationships between entities.
|
||||
- Share link via base64 for smaller graphs
|
||||
- Undo/Redo support
|
||||
- PWA support (installable, offline-capable)
|
||||
- Self-hostable
|
||||
- Desktop App support (via Wails)
|
||||
- Single Binary Web Server (via Go)
|
||||
- Mobile support
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
### Go Binary
|
||||
|
||||
The easiest way to self-host is using the single binary:
|
||||
|
||||
```sh
|
||||
./linking-tool --port 8080
|
||||
```
|
||||
|
||||
### NPM
|
||||
|
||||
```sh
|
||||
@@ -25,23 +34,23 @@ npm config set @quad4:registry https://git.quad4.io/api/packages/quad4-software/
|
||||
npm install -g @quad4/linking-tool
|
||||
linking-tool
|
||||
```
|
||||
Or
|
||||
|
||||
```sh
|
||||
PORT=3000 HOST=0.0.0.0 linking-tool
|
||||
```
|
||||
### Docker
|
||||
|
||||
```sh
|
||||
docker run -p 3000:3000 git.quad4.io/quad4-software/linking-tool
|
||||
docker run -p 8080:8080 git.quad4.io/quad4-software/linking-tool
|
||||
```
|
||||
|
||||
### Podman
|
||||
## Desktop Application
|
||||
|
||||
You can build the desktop application for your platform using Wails:
|
||||
|
||||
```sh
|
||||
podman run -p 3000:3000 git.quad4.io/quad4-software/linking-tool
|
||||
make desktop-build
|
||||
```
|
||||
|
||||
The binary will be located in `bin/`.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
@@ -49,31 +58,29 @@ git clone https://git.quad4.io/quad4-software/linking-tool.git
|
||||
cd linking-tool
|
||||
```
|
||||
|
||||
### NPM
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Makefile
|
||||
|
||||
```sh
|
||||
make dev
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Uses Chainguard Images which are rootless and very minimal images.
|
||||
The project uses a Makefile for all common tasks:
|
||||
|
||||
```sh
|
||||
docker build -t quad4-linking-tool .
|
||||
docker run -p 3000:3000 quad4-linking-tool
|
||||
make dev # Run development servers (Go & SvelteKit)
|
||||
make build # Build the single binary web server
|
||||
make help # List all available targets
|
||||
```
|
||||
|
||||
### Docker Build & Artifact Extraction
|
||||
|
||||
If you don't have the development environment (Go, Node, Wails) installed locally, you can build and extract binaries using Docker:
|
||||
|
||||
```sh
|
||||
make docker-builder
|
||||
```
|
||||
|
||||
This will build the server and desktop application inside a container and copy the resulting binaries to the `bin/` directory on your host machine.
|
||||
|
||||
## Contributing
|
||||
|
||||
Send us a email at[team@quad4.io](mailto:team@quad4.io) for any issues or feedback.
|
||||
Send us an email at [team@quad4.io](mailto:team@quad4.io) for any issues or feedback.
|
||||
|
||||
## LICENSE
|
||||
|
||||
|
||||
159
desktop/app.go
Normal file
159
desktop/app.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
port int
|
||||
debug bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App struct
|
||||
func NewApp(debug bool) *App {
|
||||
return &App{
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) logDebug(format string, args ...any) {
|
||||
if a != nil && a.debug {
|
||||
fmt.Printf("[debug] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// logHandler wraps HTTP handlers to log requests when debug is enabled.
|
||||
func (a *App) logHandler(next http.Handler) http.Handler {
|
||||
if !a.debug {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
fmt.Printf("[debug] http %s %s %dms\n", r.Method, r.URL.Path, time.Since(start).Milliseconds())
|
||||
})
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.logDebug("startup begin")
|
||||
|
||||
// Start local API server on a random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting local server: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.port = listener.Addr().(*net.TCPAddr).Port
|
||||
a.logDebug("local API listener bound on %s", listener.Addr().String())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// CORS middleware for local desktop API
|
||||
cors := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: a.logHandler(mux),
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Printf("Error serving desktop API: %v\n", err)
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Desktop API server started on port %d\n", a.port)
|
||||
a.logDebug("startup complete")
|
||||
}
|
||||
|
||||
// GetAPIPort returns the port the local server is running on
|
||||
func (a *App) GetAPIPort() int {
|
||||
a.logDebug("GetAPIPort -> %d", a.port)
|
||||
return a.port
|
||||
}
|
||||
|
||||
// LogFrontend allows the frontend to log to the terminal
|
||||
func (a *App) LogFrontend(message string) {
|
||||
fmt.Printf("[frontend] %s\n", message)
|
||||
}
|
||||
|
||||
// SaveFile shows a save dialog and writes the content to the selected file
|
||||
func (a *App) SaveFile(filename string, content string) error {
|
||||
a.logDebug("SaveFile filename=%s", filename)
|
||||
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
DefaultFilename: filename,
|
||||
Title: "Save Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filePath == "" {
|
||||
return nil // Cancelled
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// LoadFile shows an open dialog and returns the content of the selected file
|
||||
func (a *App) LoadFile() (string, error) {
|
||||
a.logDebug("LoadFile")
|
||||
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Open Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if filePath == "" {
|
||||
return "", nil // Cancelled
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
53
desktop/main.go
Normal file
53
desktop/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend_dist
|
||||
var assets embed.FS
|
||||
|
||||
func debugEnabled() bool {
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "--debug" || arg == "-d" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
debug := debugEnabled()
|
||||
if debug {
|
||||
println("Debug logging enabled")
|
||||
}
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp(debug)
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Linking Tool",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
EnableDefaultContextMenu: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
15
desktop/wails.json
Normal file
15
desktop/wails.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Linking Tool",
|
||||
"assetdir": "frontend_dist",
|
||||
"frontend:dir": "..",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||
"outputfilename": "linking-tool",
|
||||
"author": {
|
||||
"name": "Quad4",
|
||||
"email": "dev@quad4.io"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,21 @@ export default [
|
||||
URLSearchParams: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
localStorage: 'readonly',
|
||||
sessionStorage: 'readonly',
|
||||
Response: 'readonly',
|
||||
Request: 'readonly',
|
||||
Headers: 'readonly',
|
||||
FormData: 'readonly',
|
||||
ServiceWorkerRegistration: 'readonly',
|
||||
location: 'readonly',
|
||||
history: 'readonly',
|
||||
addEventListener: 'readonly',
|
||||
removeEventListener: 'readonly',
|
||||
requestAnimationFrame: 'readonly',
|
||||
queueMicrotask: 'readonly',
|
||||
atob: 'readonly',
|
||||
btoa: 'readonly',
|
||||
alert: 'readonly',
|
||||
@@ -89,10 +103,12 @@ export default [
|
||||
fetch: 'readonly',
|
||||
URL: 'readonly',
|
||||
console: 'readonly',
|
||||
Response: 'readonly',
|
||||
Request: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**', 'archive/**'],
|
||||
ignores: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**', 'archive/**', 'desktop/frontend_dist/**', 'wailsjs/**'],
|
||||
},
|
||||
];
|
||||
|
||||
35
go.mod
Normal file
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module git.quad4.io/Quad4-Software/linking-tool
|
||||
|
||||
go 1.24
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.11.0
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
81
go.sum
Normal file
81
go.sum
Normal file
@@ -0,0 +1,81 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
141
main.go
Normal file
141
main.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed build/*
|
||||
var buildAssets embed.FS
|
||||
|
||||
func corsMiddleware(allowedOrigins []string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
allowed := false
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowed = true
|
||||
} else {
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" || o == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
}
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
if allowed {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed && len(allowedOrigins) > 0 {
|
||||
log.Printf("Blocked CORS request from origin: %s", origin)
|
||||
http.Error(w, "CORS Origin Not Allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
frontendPath := flag.String("frontend", "", "Path to custom frontend build directory (overrides embedded assets)")
|
||||
host := flag.String("host", "0.0.0.0", "Host to bind the server to")
|
||||
port := flag.String("port", "", "Port to listen on (overrides PORT env var)")
|
||||
allowedOriginsStr := flag.String("allowed-origins", os.Getenv("ALLOWED_ORIGINS"), "Comma-separated list of allowed CORS origins")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var allowedOrigins []string
|
||||
if *allowedOriginsStr != "" {
|
||||
origins := strings.Split(*allowedOriginsStr, ",")
|
||||
for _, o := range origins {
|
||||
allowedOrigins = append(allowedOrigins, strings.TrimSpace(o))
|
||||
}
|
||||
}
|
||||
|
||||
if *port == "" {
|
||||
*port = os.Getenv("PORT")
|
||||
if *port == "" {
|
||||
*port = "8080"
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware chains
|
||||
cors := corsMiddleware(allowedOrigins)
|
||||
|
||||
http.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}))
|
||||
|
||||
// Static Assets
|
||||
var staticFS fs.FS
|
||||
if *frontendPath != "" {
|
||||
log.Printf("Using custom frontend from: %s\n", *frontendPath)
|
||||
staticFS = os.DirFS(*frontendPath)
|
||||
} else {
|
||||
sub, err := fs.Sub(buildAssets, "build")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
staticFS = sub
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(staticFS))
|
||||
|
||||
// SPA Handler
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
_, err := staticFS.Open(path)
|
||||
if err != nil {
|
||||
// If file doesn't exist, serve index.html for SPA routing
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
addr := net.JoinHostPort(*host, *port)
|
||||
log.Printf("Linking Tool server starting on %s...\n", addr)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: nil,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
165
package-lock.json
generated
165
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@quad4/linking-tool",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@quad4/linking-tool",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.23",
|
||||
"lucide-svelte": "^0.562.0",
|
||||
@@ -18,8 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
@@ -820,112 +819,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
"version": "28.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz",
|
||||
"integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"commondir": "^1.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"fdir": "^6.2.0",
|
||||
"is-reference": "1.2.1",
|
||||
"magic-string": "^0.30.3",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 || 14 >= 14.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.68.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-json": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
||||
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||
@@ -1250,32 +1143,16 @@
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.0.tgz",
|
||||
"integrity": "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==",
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
|
||||
"integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-node": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.4.0.tgz",
|
||||
"integrity": "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"rollup": "^4.9.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.49.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz",
|
||||
@@ -1374,13 +1251,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.50.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz",
|
||||
@@ -1960,13 +1830,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2400,13 +2263,6 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@@ -2723,13 +2579,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@quad4/linking-tool",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
"bin": {
|
||||
@@ -20,20 +20,20 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
|
||||
"prebuild": "node scripts/inject-sw-version.js",
|
||||
"build": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
|
||||
"preview": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite preview",
|
||||
"start": "HOST=127.0.0.1 PORT=3000 node build",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"package": "svelte-kit sync && vite build"
|
||||
"package": "svelte-kit sync && vite build",
|
||||
"desktop:dev": "make desktop-dev",
|
||||
"desktop:build": "make desktop-build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
|
||||
1
package.json.md5
Executable file
1
package.json.md5
Executable file
@@ -0,0 +1 @@
|
||||
6da4cdcafa6966a9d35d5e1ce48583eb
|
||||
24
scripts/inject-sw-version.js
Normal file
24
scripts/inject-sw-version.js
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
|
||||
const version = packageJson.version;
|
||||
|
||||
const swPath = join(rootDir, 'static', 'sw.js');
|
||||
let swContent = readFileSync(swPath, 'utf-8');
|
||||
|
||||
swContent = swContent.replace(
|
||||
/const CACHE_VERSION = ['"](.*?)['"];/,
|
||||
`const CACHE_VERSION = '${version}';`
|
||||
);
|
||||
|
||||
writeFileSync(swPath, swContent);
|
||||
console.log(`Injected version ${version} into service worker`);
|
||||
|
||||
13
src/app.d.ts
vendored
13
src/app.d.ts
vendored
@@ -8,6 +8,19 @@ declare global {
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
go?: {
|
||||
main: {
|
||||
App: {
|
||||
SaveFile(filename: string, content: string): Promise<string>;
|
||||
LoadFile(): Promise<string>;
|
||||
LogFrontend(message: string): void;
|
||||
};
|
||||
};
|
||||
};
|
||||
runtime?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -577,13 +577,23 @@
|
||||
links,
|
||||
};
|
||||
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
const defaultFilename = `quad4-linking-graph-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
|
||||
try {
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
if (window.go?.main?.App?.SaveFile) {
|
||||
// Wails desktop export
|
||||
const err = await window.go.main.App.SaveFile(defaultFilename, jsonData);
|
||||
if (err) throw new Error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Web browser export
|
||||
const blob = new Blob([jsonData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `quad4-linking-graph-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
link.download = defaultFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
@@ -596,6 +606,32 @@
|
||||
|
||||
async function importGraph() {
|
||||
try {
|
||||
if (window.go?.main?.App?.LoadFile) {
|
||||
// Wails desktop import
|
||||
const text = await window.go.main.App.LoadFile();
|
||||
if (!text) return; // Cancelled
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (parseErr) {
|
||||
alert(`Failed to parse JSON: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.nodes && data.links) {
|
||||
pushState();
|
||||
nodes = normalizeNodes(data.nodes);
|
||||
links = data.links;
|
||||
centerView();
|
||||
saveToLocalStorage();
|
||||
} else {
|
||||
alert('Invalid graph format: file must contain nodes and links arrays');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Web browser import
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
@@ -1226,6 +1262,15 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const log = (msg: string) => {
|
||||
console.log(msg);
|
||||
if (window.go?.main?.App?.LogFrontend) {
|
||||
window.go.main.App.LogFrontend(msg);
|
||||
}
|
||||
};
|
||||
|
||||
log('Linking Tool started');
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile = window.innerWidth < 640;
|
||||
};
|
||||
@@ -1238,6 +1283,13 @@
|
||||
if (!loadFromUrl()) {
|
||||
loadFromLocalStorage();
|
||||
}
|
||||
|
||||
// Wails detection
|
||||
const isWails = window.runtime || window.go;
|
||||
if (isWails) {
|
||||
log('Wails environment detected');
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkMobile);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,62 @@
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let showUpdateAvailable = false;
|
||||
let registration: ServiceWorkerRegistration | null = null;
|
||||
|
||||
function checkForUpdates() {
|
||||
if (registration && navigator.onLine) {
|
||||
registration.update().catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reloadApp() {
|
||||
if (registration && registration.waiting) {
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('Service Worker registered:', registration);
|
||||
.then((reg) => {
|
||||
registration = reg;
|
||||
|
||||
reg.addEventListener('updatefound', () => {
|
||||
const newWorker = reg.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed') {
|
||||
if (reg.waiting) {
|
||||
showUpdateAvailable = true;
|
||||
} else if (navigator.serviceWorker.controller) {
|
||||
showUpdateAvailable = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (reg.waiting) {
|
||||
showUpdateAvailable = true;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
if (navigator.onLine) {
|
||||
setInterval(() => {
|
||||
checkForUpdates();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
checkForUpdates();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
@@ -16,4 +66,43 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showUpdateAvailable}
|
||||
<div
|
||||
class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-neutral-900 border border-neutral-800 rounded-lg shadow-lg p-4 max-w-md mx-4"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-white">Update Available</p>
|
||||
<p class="text-xs text-neutral-400 mt-1">A new version is available. Reload to update.</p>
|
||||
</div>
|
||||
<button
|
||||
on:click={reloadApp}
|
||||
class="px-4 py-2 bg-accent-red text-white rounded-md text-sm font-medium hover:bg-accent-red-dark transition-colors"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (showUpdateAvailable = false)}
|
||||
class="text-neutral-400 hover:text-white transition-colors"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
|
||||
35
static/sw.js
35
static/sw.js
@@ -1,4 +1,5 @@
|
||||
const CACHE_NAME = 'quad4-linking-tool-v1';
|
||||
const CACHE_VERSION = '1.4.0';
|
||||
const CACHE_NAME = `quad4-linking-tool-${CACHE_VERSION}`;
|
||||
const urlsToCache = ['/', '/favicon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
@@ -17,7 +18,7 @@ self.addEventListener('activate', (event) => {
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
if (cacheName !== CACHE_NAME && cacheName.startsWith('quad4-linking-tool-')) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
@@ -27,26 +28,40 @@ self.addEventListener('activate', (event) => {
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.request.url.startsWith(self.location.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request).then((response) => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return fetch(event.request)
|
||||
.then((response) => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('/') || new Response('Offline', { status: 503 });
|
||||
});
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@@ -6,8 +6,14 @@ const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user