Compare commits

..
25 Commits
Author SHA1 Message Date
John O'Keefe 5ce1094b90 fix(release): make version bump re-runnable when already current
Release / release (push) Successful in 2m33s
Re-running make release for an already-bumped version died twice: the config.yml python asserted on change (no-op rewrite tripped it), and git commit would fail on an empty stage next. Now the python asserts only the pattern presence (real file-shape errors still abort) and prints already-at-version, and the commit step skips via git diff --cached when the stage is empty. Re-runs proceed to cliff notes and tagging instead of aborting. Proven both branches: already-current is a clean no-op, stale value bumps exactly line 13 with the schema version untouched.
2026-09-16 09:23:26 -04:00
John O'Keefe 0b9f19cb08 chore(release): bump version to 1.99.1
Release / release (push) Successful in 2m14s
Version bump for the updater live-proof cycle (NOT a release: no tag). Carries the signed-asset CI path so the 1.99.1-rc1 pre-release publishes AniTrack-linux-amd64 plus sidecars for the beta-channel end-to-end test.
2026-09-16 08:55:31 -04:00
John O'Keefe 948c2d3960 docs: record self-updates design and correct branch map
Add the Self-updates section (provider, signing, gates, beta channel, binary-only scope, key custody) and fix the two stale branch-map lines that still described main as the v2 line. History above untouched.
2026-09-16 08:52:14 -04:00
John O'Keefe 0149b24a33 ci(release): publish signed updater assets alongside tarball
After packaging, stage the bare AniTrack-linux-amd64 binary (the updater swaps os.Executable in place; the user tarball single top-level directory would fail the single-entry rule), sign it with wails3 updater sign using UPDATER_SIGNING_KEY (missing secret is a hard error, key shredded after use), split the output into .sha512/.sig sidecars, and extend the idempotent replace-upload loop from 1 asset to all 4. Tarball flow untouched; rc tags get signed assets too, which the beta-channel live proof needs.
2026-09-16 08:52:09 -04:00
John O'Keefe 52fc656669 feat(updater): wire startup check for desktop production builds
maybeEnableUpdater inits app.Updater (CurrentVersion from wails.json, embedded public key, accent-themed builtin window) behind two gates: -tags production (wails3 build sets it, dev does not) and application.System.IsDesktop (mobile stays on Obtainium). The startup check is headless; the window opens via a second CheckAndInstall only when an update is found. ANITRACK_UPDATER_CHANNEL=beta opts into -rc pre-releases for testing. updater_flow_test.go drives the REAL framework verifier headless: good signature stages byte-identical, tampered signature rejected (both phases green). Works around the framework process-wide app singleton with a single two-phase test.
2026-09-16 08:52:04 -04:00
John O'Keefe e9b04c0a84 feat(updater): custom Gitea releases provider with fail-closed verification
Wails ships no Gitea provider, so add updater/gitea (~250 lines): latest non-draft release newer than current wins, pre-releases skipped unless AllowPrerelease, exact AniTrack-linux-amd64 asset match, .sha512/.sig sidecars parsed into updater.Verification (sha512+ed25519ph, mirroring wails3 updater sign). Anything missing or malformed is an error, never an unsigned install. Includes semver compare, streaming download with progress, and unit tests (picking rules, fail-closed sidecars, server errors, progress) plus an opt-in live probe (ANITRACK_LIVE_TEST=1) proven against the real instance. Also: embed updater.pub (public half; private key lives in password manager + UPDATER_SIGNING_KEY CI secret, updater.key gitignored) and ignore CI-side updater-dist/ staging.
2026-09-16 08:51:59 -04:00
John O'Keefe bc1f4b1482 docs: point runners at install script, document self-updates
Runtime box now leads with the tarball install_linux.sh (which installs the webkit runtime for supported distros) and keeps manual per-distro commands as fallback. Add an Updates section: desktop Linux releases self-update in-app (signed, verified), binary-only; icons and .desktop still come from the tarball.
2026-09-16 08:51:52 -04:00
John O'Keefe 5036e34d3c fix(install): install webkitgtk-6.0 runtime per distro, refuse on musl
The release binary dynamically links libwebkitgtk-6.0, which is not preinstalled on most distros and is not shipped in the tarball, so installs failed at first launch with a missing-.so error. Add a step-0 dependency stage to install_linux.sh that maps Arch/Debian-Ubuntu/Fedora/OpenMandriva/Void to their verified runtime package names (ID plus ID_LIKE, so derivatives resolve), requires root/sudo, and stays idempotent. musl systems (Alpine, Void-musl) get a warning plus hard-abort: the glibc-linked binary cannot run there and no package fixes that. Unknown distros get the package table and exit 1. Existing desktop/icons/binary steps untouched. Verified with an 11-case stub harness (all families route correctly; sudo/no-sudo/missing-manager/musl/unknown all behave).
2026-09-16 08:51:47 -04:00
John O'Keefe 8bae7f65f8 chore(release): bump version to 1.99.0
Release / release (push) Successful in 3m8s
2026-09-15 22:29:43 -04:00
John O'Keefe 4d31edad4d docs: record dual version-file release automation
Append one sentence to the V3_MIGRATION build record noting make release now bumps both wails.json and build/config.yml info.version (TEMP until wails.json retires). Keeps the migration doc from contradicting the automated flow; history above the line untouched.
2026-09-15 22:29:25 -04:00
John O'Keefe ba2b49bedd chore(release): bump build/config.yml alongside wails.json
make release only bumped wails.json, leaving build/config.yml info.version stale (it sat at 1.6.8 while wails.json moved to 1.99.0). Add a companion python one-liner that rewrites only the indented info.version line - the regex cannot match the top-level schema version 3 - and stage both files into the same bump commit. Marked TEMP until wails.json (v2 leftover) is retired. The companion NOTE in build/config.yml is updated to describe the automation and warn off the schema line. Verified with make -n dry-run and a scratch-copy regex proof (schema line byte-identical, single-line diff).
2026-09-15 22:29:13 -04:00
John O'Keefe 4c34a57493 chore(release): bump version to 1.99.0
Release / release (push) Canceled after 2m11s
2026-09-15 22:20:59 -04:00
John O'Keefe 2881370acd docs: list webkitgtk-6.0 build packages and open 1.99.x beta series
Source builders need the WebKitGTK 6 dev stack, which replaces the v2-era 4.x packages and is not preinstalled anywhere: give copy-paste distro commands (Arch/Debian-Ubuntu, names verified against the repos and the release workflow) plus the pinned wails3 CLI install. Same section records the version convention: 1.99.x is the v3-migration beta series (Wails v3 live, Svelte 4/Vite 4 refresh pending), 2.0.0 reserved for the finished article.
2026-09-15 22:19:27 -04:00
John O'Keefe e3eecc5b96 docs: note webkitgtk-6.0 runtime requirement for running releases
The release binary dynamically links libwebkitgtk-6.0 and libgtk-4, neither of which ships in the tarball or gets installed by install_linux.sh, and webkitgtk-6.0 is not preinstalled on most distros. Tell runners up front which runtime package to install (Arch/Debian-Ubuntu names verified) and what the missing-.so launch error looks like.
2026-09-15 22:18:52 -04:00
John O'Keefe 640e31f900 fix(ui): move refresh button into watchlist header next to sort
Release / release (push) Successful in 3m35s
Relocate RefreshWatchListButton from the Home route into the WatchList header row beside Sort, so refresh lives with the list it refreshes. Tweak header styling to fit the new layout (text-nowrap on the button, max-h/margins on the sort select).
2026-09-15 21:35:49 -04:00
John O'Keefe fc29c696ce chore: ignore frontend/package.json.md5 artifact
The Wails dev flow regenerates frontend/package.json.md5 (package.json change-detection hash) on every run, so it showed as untracked after each make dev. Scoped to frontend/; the tracked frontend/package-lock.json rule is unaffected.
2026-09-15 21:35:41 -04:00
John O'Keefe 1d4fa38b8e fix(linux): use OnDemand GPU policy for AMD/Intel Mesa
WebKitGTK 6.0 removed WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND, so Wails maps OnDemand to ALWAYS. Never (the old Nvidia/X11 blank-window workaround) forces CPU raster on the GTK4/WebKit 6 stack. All target machines are AMD or Intel, where the Nvidia DMABUF workaround is unnecessary, so let the GPU handle scrolling/compositing.
2026-09-15 21:35:33 -04:00
John O'Keefe 01299994b4 docs: clarify home branches for v3 follow-up work
The Future plans header (Not in this branch) read as if follow-up work were banned from wailsv3. Reword to Not yet done and give each item an explicit home: stabilization + toolchain majors stay on wailsv3, Android goes to a short-lived wailsv3-android feature branch so mobile scaffolding does not pollute the desktop-to-main merge, CI follow-ups live with whatever they support.
2026-09-13 21:56:38 -04:00
John O'Keefe 61813a4391 chore: merge main (ignore .task cache) into wailsv3 2026-09-13 18:01:14 -04:00
John O'Keefe 2b4eb31495 docs: summarize v3 migration and note stale v2 key entries
Add docs/V3_MIGRATION.md — a full write-up of the wailsv3 desktop migration (backend/frontend/build/CI changes, verification, branch map) and the planned follow-ups (toolchain majors + Android sideload). No behavior change.

Add a short notice to README.md: Migrating from v2 — v2 and v3 Secret Service collections coexist (AniTrack vs login) so both versions keep working after the first v3 re-login; the orphaned v2 entries are harmless. Details in docs/V3_MIGRATION.md. No cleanup instructions, by request.
2026-09-13 17:52:53 -04:00
John O'Keefe 2ab0843a0c chore(wailsv3): v3 conformance pass over project files
Fixes the wails3 dev boot failure and removes remaining v2-isms so the tree matches a stock v3 project.

- frontend/package.json: add missing build:dev script (vite build --minify false --mode development). The dev taskflow runs npm run build:dev, which v2-era package.json never had - this was the wails3 dev boot failure.
- frontend/vite.config.ts: add the @wailsio/runtime vite plugin (typed custom events, as in every v3 template) and the server host/port/strictPort block (5173, matching the dev task).
- Remove stale v2 build dirs build/darwin + build/windows (v2 wails scaffolding; v3 generates its own via build-assets if ever needed).
- Delete untracked frontend/package.json.md5 (v2 change-detection artifact; v3 Taskfile uses content-hash sources) and drop its gitignore rule.
- go.mod: delete the leftover commented-out v2 replace directive.
- README.md: dev/build docs updated to make dev / make build (wails3), dropping the v2 :34115 browser-server paragraph; install link points at v3 docs.

Verified: production vite build green with the plugin, and wails3 dev boots end-to-end (bindings regen, build:dev, vite on :5173, asset server connected, AniTrack 1.6.8 window mapped).
2026-09-13 17:20:19 -04:00
John O'Keefe 1fa0e4afaa ci(release): build releases with the Wails v3 toolchain
Release flow (make build -> build/bin/AniTrack -> tarball) is unchanged; only the toolchain moves to v3.

- Wails CLI: v2@v2.15.0 -> wails3@v3.0.0-beta.20 (binary path, cache key, and install pin updated together).
- System deps: libgtk-3-dev/libwebkit2gtk-4.1-dev -> libgtk-4-dev/libwebkitgtk-6.0-dev for the v3 default GTK4/WebKit 6.0 stack.
- Unchanged and still valid: Go/npm/go-build/Wails-CLI caches (npm cache now resolves since the frontend lockfile is committed), environment.go generation, version guard, idempotent release+asset upload.
2026-09-13 17:10:25 -04:00
John O'Keefe 7d13b5d901 chore(wailsv3): add v3 Taskfile build scaffolding and config
Desktop-Linux-only scaffolding adapted from the stock v3 svelte template; no mobile taskfiles added yet.

- Taskfile.yml (root): APP_NAME AniTrack, BIN_DIR build/bin (keeps the v2 output path so release packaging is untouched), includes common+linux only, build/run/dev tasks.
- build/Taskfile.yml: stock common tasks (frontend build, bindings regen, go mod tidy).
- build/linux/Taskfile.yml: stock, minus the common:generate:icons dep (it targets macOS/Windows icons from a build/appicon.png this project does not have; icons ship via build/icon + the release tarball). Generated build/linux/AniTrack.desktop committed (stable output).
- build/config.yml: AniTrack metadata, version 1.6.8. NOTE: ./release only bumps wails.json productVersion, not this file - bump both on release until the script learns about it.
- wails.json: v3 frontend block (dir/install/build/dev/devServerUrl :5173); author + info.productVersion kept for the in-app title and the release version guard.
- Makefile: wails dev/build -> wails3 dev -port 5173 / wails3 build. The v2 webkit2_41 tag is a v2-ism v3 ignores; v3 defaults to GTK4/WebKitGTK 6.0 (same 2.52.x engine generation), so no tags.
- .gitignore: task cache dir .task/ ignored.
2026-09-13 17:10:21 -04:00
John O'Keefe 0642163844 chore(wailsv3): migrate frontend to v3 bindings and runtime
Regenerated bindings with wails3 generate bindings (1 service, 29 methods, 25 models) into frontend/bindings/AniTrack as TypeScript; output verified byte-deterministic across regens, so it is committed like wailsjs was.

- 10 Svelte files: wailsjs/go/main/App imports -> import {App} from bindings/AniTrack with App.* call prefixes. No logic or markup changes.
- WebsiteLink.svelte: BrowserOpenURL -> Browser.OpenURL from @wailsio/runtime.
- AvatarMenu.svelte: runtime.Quit -> Application.Quit from @wailsio/runtime.
- package.json: add @wailsio/runtime pinned to 3.0.0-beta.20 (exact, matching the Go framework); lockfile updated, all deps still max in-range, majors deferred.
- Production vite build passes with the new bindings.
2026-09-13 17:10:17 -04:00
John O'Keefe 850c1c6aee chore(wailsv3): migrate Go backend from Wails v2 to v3
Minimal-structure migration to Wails v3.0.0-beta.20 (CLI-matching pin). Single App service preserved; no domain logic touched.

- main.go: wails.Run(options.App) -> application.New + RegisterService + Window.NewWithOptions. Window keeps title-with-version, 1024x768, RGBA background, Linux icon/translucency/GpuPolicyNever, ProgramName. SingleInstanceLock -> SingleInstanceOptions (same UniqueID); callback closes over the service variable set right after New.
- app.go: App holds *application.App instead of a context. startup(ctx) is gone; the version title moves into the window options via appTitle(). onSecondInstanceLaunch takes v3 SecondInstanceData (Args/WorkingDir) and uses Window.Current() Restore+Focus plus Event.Emit for launchArgs. ShowVersion uses Dialog.Info.
- AniList/MAL/Simkl *UserFunctions.go: runtime.BrowserOpenURL -> app.Browser.OpenURL (errors logged), runtime.MessageDialog -> app.Dialog.Info chains. OAuth callback servers, token flows, and key names unchanged.
- Secrets: 99designs/keyring replaced with zalando/go-keyring (what v3 itself depends on) under the same AniTrack service name, so existing stored tokens keep working. Per-file Set helpers kept; Open/Ready boilerplate deleted (zalando needs no handle).
- go.mod: v2 + 99designs dropped (stale v2 replace directive removed), v3 beta.20 + zalando v0.2.8 added.
2026-09-13 17:10:09 -04:00
61 changed files with 2506 additions and 2852 deletions
+61 -26
View File
@@ -106,9 +106,10 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
sudo apt-get update sudo apt-get update
# v3 desktop defaults to the GTK4/WebKitGTK 6.0 stack.
sudo apt-get install -y \ sudo apt-get install -y \
build-essential pkg-config \ build-essential pkg-config \
libgtk-3-dev libwebkit2gtk-4.1-dev \ libgtk-4-dev libwebkitgtk-6.0-dev \
jq jq
- name: Cache Wails CLI - name: Cache Wails CLI
@@ -117,14 +118,14 @@ jobs:
with: with:
# The compiled CLI binary. Bump the key whenever the @version pin # The compiled CLI binary. Bump the key whenever the @version pin
# below changes, or the old CLI will be silently reused. # below changes, or the old CLI will be silently reused.
path: ~/go/bin/wails path: ~/go/bin/wails3
key: wails-v2.15.0-${{ runner.os }} key: wails3-v3.0.0-beta.20-${{ runner.os }}
- name: Install Wails CLI - name: Install Wails CLI
if: steps.wails-cli.outputs.cache-hit != 'true' if: steps.wails-cli.outputs.cache-hit != 'true'
run: | run: |
set -euo pipefail set -euo pipefail
go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0 go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.20
- name: Add Go bin to PATH - name: Add Go bin to PATH
run: echo "${HOME}/go/bin" >> "${GITHUB_PATH}" run: echo "${HOME}/go/bin" >> "${GITHUB_PATH}"
@@ -244,7 +245,38 @@ jobs:
echo "Created new release for ${TAG}" echo "Created new release for ${TAG}"
fi fi
- name: Upload release archive - name: Build updater asset and sign
env:
UPDATER_SIGNING_KEY: ${{ secrets.UPDATER_SIGNING_KEY }}
run: |
set -euo pipefail
: "${VERSION:?VERSION missing from version-guard step}"
export PATH="${HOME}/go/bin:${PATH}"
: "${UPDATER_SIGNING_KEY:?Missing secret UPDATER_SIGNING_KEY. Add the updater private key under Settings → Secrets → Actions.}"
# Bare updater binary: the updater swaps os.Executable() in place,
# so it takes the single binary, not the user tarball (whose
# top-level directory would fail the single-entry rule).
mkdir -p updater-dist
cp build/bin/AniTrack updater-dist/AniTrack-linux-amd64
printf '%s' "${UPDATER_SIGNING_KEY}" > updater-dist/updater.key
chmod 600 updater-dist/updater.key
wails3 updater sign -key updater-dist/updater.key updater-dist/AniTrack-linux-amd64 > updater-dist/sign.json
# Split the sign output into the sidecar assets the Gitea provider
# fetches: <name>.sha512 (sha512sum format) + <name>.sig (base64).
python3 - <<'EOF'
import base64, binascii, json
entries = json.load(open('updater-dist/sign.json'))
entry = next(e for e in entries if e['filename'].endswith('AniTrack-linux-amd64'))
assert entry['digestAlgo'] == 'sha512', entry
assert entry['signatureAlgo'] == 'ed25519ph', entry
digest_hex = binascii.hexlify(base64.b64decode(entry['digest'])).decode()
open('updater-dist/AniTrack-linux-amd64.sha512', 'w').write(f"{digest_hex} AniTrack-linux-amd64\n")
open('updater-dist/AniTrack-linux-amd64.sig', 'w').write(entry['signature'].strip() + '\n')
EOF
shred -u updater-dist/updater.key
ls -la updater-dist/
- name: Upload release assets
env: env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }} TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REPO: ${{ gitea.repository }} REPO: ${{ gitea.repository }}
@@ -254,27 +286,30 @@ jobs:
: "${ARCHIVE:?ARCHIVE missing from packaging step}" : "${ARCHIVE:?ARCHIVE missing from packaging step}"
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases" API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
AUTH="Authorization: token ${TOKEN}" AUTH="Authorization: token ${TOKEN}"
test -f "${ARCHIVE}" || { echo "::error::${ARCHIVE} not found" >&2; exit 1; } for ASSET in "${ARCHIVE}" updater-dist/AniTrack-linux-amd64 updater-dist/AniTrack-linux-amd64.sha512 updater-dist/AniTrack-linux-amd64.sig; do
test -f "${ASSET}" || { echo "::error::${ASSET} not found" >&2; exit 1; }
RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')" RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')"
if [ -z "${RID}" ]; then if [ -z "${RID}" ]; then
echo "::error::No release found for tag ${TAG} after create step" >&2 echo "::error::No release found for tag ${TAG} after create step" >&2
exit 1 exit 1
fi fi
# Replace a same-named asset so re-runs stay idempotent. # Replace a same-named asset so re-runs stay idempotent.
AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ARCHIVE}" '.[] | select(.name==$n) | .id // empty')" ANAME="$(basename "${ASSET}")"
if [ -n "${AID}" ]; then AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ANAME}" '.[] | select(.name==$n) | .id // empty')"
curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null if [ -n "${AID}" ]; then
echo "Deleted existing asset id=${AID} (${ARCHIVE})" curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null
fi echo "Deleted existing asset id=${AID} (${ANAME})"
fi
resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \ resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \
-F "attachment=@${ARCHIVE}" "${API}/${RID}/assets?name=${ARCHIVE}")" -F "attachment=@${ASSET}" "${API}/${RID}/assets?name=${ANAME}")"
code="$(printf '%s' "${resp}" | tail -n1)" code="$(printf '%s' "${resp}" | tail -n1)"
rbody="$(printf '%s' "${resp}" | sed '$d')" rbody="$(printf '%s' "${resp}" | sed '$d')"
if [ "${code}" -ge 400 ]; then if [ "${code}" -ge 400 ]; then
echo "::error::Asset upload ${code}: ${rbody}" >&2 echo "::error::Asset upload ${code}: ${rbody}" >&2
exit 1 exit 1
fi fi
echo "Uploaded ${ARCHIVE} to release id=${RID}" echo "Uploaded ${ANAME} to release id=${RID}"
done
+6 -1
View File
@@ -26,13 +26,16 @@ build/bin
.task/ .task/
node_modules node_modules
frontend/dist frontend/dist
package.json.md5 frontend/package.json.md5
# Root-level lockfile only: the frontend lockfile IS tracked so CI installs # Root-level lockfile only: the frontend lockfile IS tracked so CI installs
# are reproducible and setup-node's npm cache has something to hash. # are reproducible and setup-node's npm cache has something to hash.
/package-lock.json /package-lock.json
.idea .idea
.env .env
environment.go environment.go
# Updater signing private key. The public half (updater.pub) is committed and
# embedded; the private key lives in the password manager + CI secrets only.
updater.key
# REST (http files) # REST (http files)
http-client.private.env.json http-client.private.env.json
@@ -41,3 +44,5 @@ http-client.private.env.json
*.tar *.tar
*.tar.gz *.tar.gz
/AniTrack /AniTrack
# Updater CI staging (bare binary + sidecars, runner-ephemeral)
updater-dist/
+38 -74
View File
@@ -13,41 +13,17 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var aniListJwt AniListJWT var aniListJwt AniListJWT
var aniRing keyring.Keyring // aniKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
func init() { const aniKeyringService = "AniTrack"
var err error
aniRing, err = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
if err != nil {
log.Printf("anilist: secure storage unavailable: %s", err)
}
}
func aniRingReady() bool {
if aniRing == nil {
log.Println("anilist: secure storage unavailable")
return false
}
return true
}
func aniRingSet(key string, data []byte) error { func aniRingSet(key string, data []byte) error {
if !aniRingReady() { if err := keyring.Set(aniKeyringService, key, string(data)); err != nil {
return errors.New("anilist: secure storage unavailable")
}
if err := aniRing.Set(keyring.Item{Key: key, Data: data}); err != nil {
log.Printf("anilist: save %s failed: %s", key, err) log.Printf("anilist: save %s failed: %s", key, err)
return err return err
} }
@@ -58,23 +34,20 @@ var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
func (a *App) CheckIfAniListLoggedIn() bool { func (a *App) CheckIfAniListLoggedIn() bool {
if (AniListJWT{} == aniListJwt) { if (AniListJWT{} == aniListJwt) {
if !aniRingReady() { tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
return false expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
} refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
tokenType, tokenErr := aniRing.Get("anilistTokenType") accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn") if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
return false return false
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
aniListJwt.TokenType = string(tokenType.Data) aniListJwt.TokenType = tokenType
aniListJwt.AccessToken = string(accessToken.Data) aniListJwt.AccessToken = accessToken
aniListJwt.RefreshToken = string(refreshToken.Data) aniListJwt.RefreshToken = refreshToken
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
log.Printf("anilist: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr) log.Printf("anilist: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
return false return false
} }
return true return true
@@ -86,17 +59,16 @@ func (a *App) CheckIfAniListLoggedIn() bool {
func (a *App) AniListLogin() { func (a *App) AniListLogin() {
if (AniListJWT{} == aniListJwt) { if (AniListJWT{} == aniListJwt) {
if !aniRingReady() { tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
log.Println("anilist: cannot check login, secure storage unavailable") expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
return refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
} accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
tokenType, tokenErr := aniRing.Get("anilistTokenType") if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code" getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code"
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl) if err := a.app.Browser.OpenURL(getAniListCodeUrl); err != nil {
log.Printf("anilist: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
@@ -104,12 +76,12 @@ func (a *App) AniListLogin() {
serverDone.Wait() serverDone.Wait()
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
aniListJwt.TokenType = string(tokenType.Data) aniListJwt.TokenType = tokenType
aniListJwt.AccessToken = string(accessToken.Data) aniListJwt.AccessToken = accessToken
aniListJwt.RefreshToken = string(refreshToken.Data) aniListJwt.RefreshToken = refreshToken
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
log.Printf("anilist: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr) log.Printf("anilist: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
} }
} }
} }
@@ -132,17 +104,13 @@ func (a *App) handleAniListCallback(wg *sync.WaitGroup) {
_ = aniRingSet("anilistTokenExpiresIn", []byte(strconv.Itoa(aniListJwt.ExpiresIn))) _ = aniRingSet("anilistTokenExpiresIn", []byte(strconv.Itoa(aniListJwt.ExpiresIn)))
_ = aniRingSet("anilistAccessToken", []byte(aniListJwt.AccessToken)) _ = aniRingSet("anilistAccessToken", []byte(aniListJwt.AccessToken))
_ = aniRingSet("anilistRefreshToken", []byte(aniListJwt.RefreshToken)) _ = aniRingSet("anilistRefreshToken", []byte(aniListJwt.RefreshToken))
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{ a.app.Dialog.Info().
Title: "AniList Authorization", SetTitle("AniList Authorization").
Message: "It is now safe to close your browser tab", SetMessage("It is now safe to close your browser tab").
}) Show()
if err != nil {
log.Println(err)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
aniCancel() aniCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -239,14 +207,10 @@ func (a *App) GetAniListLoggedInUser() AniListUser {
func (a *App) LogoutAniList() string { func (a *App) LogoutAniList() string {
if (AniListJWT{} != aniListJwt) { if (AniListJWT{} != aniListJwt) {
if !aniRingReady() { typeErr := keyring.Delete(aniKeyringService, "anilistTokenType")
aniListJwt = AniListJWT{} expiresInErr := keyring.Delete(aniKeyringService, "anilistTokenExpiresIn")
return "AniList Logged Out Successfully" accessTokenErr := keyring.Delete(aniKeyringService, "anilistAccessToken")
} refreshTokenErr := keyring.Delete(aniKeyringService, "anilistRefreshToken")
typeErr := aniRing.Remove("anilistTokenType")
expiresInErr := aniRing.Remove("anilistTokenExpiresIn")
accessTokenErr := aniRing.Remove("anilistAccessToken")
refreshTokenErr := aniRing.Remove("anilistRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil { if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
log.Printf("anilist: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr) log.Printf("anilist: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr)
} }
+38 -74
View File
@@ -17,41 +17,17 @@ import (
"sync" "sync"
"time" "time"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var myAnimeListJwt MyAnimeListJWT var myAnimeListJwt MyAnimeListJWT
var myAnimeListRing keyring.Keyring // malKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
func init() { const malKeyringService = "AniTrack"
var err error
myAnimeListRing, err = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
if err != nil {
log.Printf("mal: secure storage unavailable: %s", err)
}
}
func malRingReady() bool {
if myAnimeListRing == nil {
log.Println("mal: secure storage unavailable")
return false
}
return true
}
func malRingSet(key string, data []byte) error { func malRingSet(key string, data []byte) error {
if !malRingReady() { if err := keyring.Set(malKeyringService, key, string(data)); err != nil {
return errors.New("mal: secure storage unavailable")
}
if err := myAnimeListRing.Set(keyring.Item{Key: key, Data: data}); err != nil {
log.Printf("mal: save %s failed: %s", key, err) log.Printf("mal: save %s failed: %s", key, err)
return err return err
} }
@@ -99,25 +75,22 @@ func (v *CodeVerifier) CodeChallengeS256() string {
func (a *App) CheckIfMyAnimeListLoggedIn() bool { func (a *App) CheckIfMyAnimeListLoggedIn() bool {
if (MyAnimeListJWT{} == myAnimeListJwt) { if (MyAnimeListJWT{} == myAnimeListJwt) {
if !malRingReady() { tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
return false expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
} accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType") refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn") if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
return false return false
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
log.Printf("mal: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr) log.Printf("mal: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
return false return false
} }
myAnimeListJwt.AccessToken = string(accessToken.Data) myAnimeListJwt.AccessToken = accessToken
myAnimeListJwt.RefreshToken = string(refreshToken.Data) myAnimeListJwt.RefreshToken = refreshToken
return true return true
} }
} else { } else {
@@ -128,31 +101,30 @@ func (a *App) CheckIfMyAnimeListLoggedIn() bool {
func (a *App) MyAnimeListLogin() { func (a *App) MyAnimeListLogin() {
if !a.CheckIfMyAnimeListLoggedIn() { if !a.CheckIfMyAnimeListLoggedIn() {
fmt.Println("check logged in function failed") fmt.Println("check logged in function failed")
if !malRingReady() { tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
log.Println("mal: cannot check login, secure storage unavailable") expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
return accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
} refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType") if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
verifier, _ := verifier() verifier, _ := verifier()
getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain" getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain"
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl) if err := a.app.Browser.OpenURL(getMyAnimeListCodeUrl); err != nil {
log.Printf("mal: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
a.handleMyAnimeListCallback(serverDone, verifier) a.handleMyAnimeListCallback(serverDone, verifier)
serverDone.Wait() serverDone.Wait()
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
log.Printf("mal: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr) log.Printf("mal: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
} }
myAnimeListJwt.AccessToken = string(accessToken.Data) myAnimeListJwt.AccessToken = accessToken
myAnimeListJwt.RefreshToken = string(refreshToken.Data) myAnimeListJwt.RefreshToken = refreshToken
} }
} }
} }
@@ -175,17 +147,13 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
_ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn))) _ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)))
_ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken)) _ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken))
_ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken)) _ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken))
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{ a.app.Dialog.Info().
Title: "MyAnimeList Authorization", SetTitle("MyAnimeList Authorization").
Message: "It is now safe to close your browser tab", SetMessage("It is now safe to close your browser tab").
}) Show()
if err != nil {
log.Println(err)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
myAnimeListCancel() myAnimeListCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -378,14 +346,10 @@ func createUser() MyAnimeListUser {
func (a *App) LogoutMyAnimeList() string { func (a *App) LogoutMyAnimeList() string {
if (MyAnimeListJWT{} != myAnimeListJwt) { if (MyAnimeListJWT{} != myAnimeListJwt) {
if !malRingReady() { typeErr := keyring.Delete(malKeyringService, "MyAnimeListTokenType")
myAnimeListJwt = MyAnimeListJWT{} expiresInErr := keyring.Delete(malKeyringService, "MyAnimeListExpiresIn")
return "MAL Logged Out Successfully" accessTokenErr := keyring.Delete(malKeyringService, "MyAnimeListAccessToken")
} refreshTokenErr := keyring.Delete(malKeyringService, "MyAnimeListRefreshToken")
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType")
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn")
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken")
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil { if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
log.Printf("mal: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr) log.Printf("mal: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr)
} }
+11 -6
View File
@@ -1,12 +1,12 @@
TAGS := webkit2_41
.PHONY: dev build clean release .PHONY: dev build clean release
# v3 uses the GTK4/WebKitGTK 6.0 stack by default (same 2.52.x engine
# generation as the v2 webkit2_41 build), so no build tags are needed.
dev: dev:
wails dev -tags $(TAGS) wails3 dev -port 5173
build: build:
wails build -tags $(TAGS) wails3 build
clean: clean:
rm -rf build/bin/* rm -rf build/bin/*
@@ -40,8 +40,13 @@ release:
@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi @if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi
@echo "Bumping wails.json to $(VERSION)..." @echo "Bumping wails.json to $(VERSION)..."
@python3 -c "import json; p='wails.json'; d=json.load(open(p)); d['info']['productVersion']='$(VERSION)'; json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')" @python3 -c "import json; p='wails.json'; d=json.load(open(p)); d['info']['productVersion']='$(VERSION)'; json.dump(d, open(p,'w'), indent=2); open(p,'a').write('\n')"
@git add wails.json # TEMP until wails.json (v2 leftover) is retired: bump build/config.yml too.
@git commit -m "chore(release): bump version to $(VERSION)" # The regex targets only the indented info.version line, never the
# top-level schema `version: '3'`.
@echo "Bumping build/config.yml to $(VERSION)..."
@python3 -c "import re; p='build/config.yml'; s=open(p).read(); m=re.search(r'^ version: \"([^\"]*)\"', s, flags=re.M); assert m, 'info.version not found'; print('build/config.yml', m.group(1), '-> $(VERSION)' if m.group(1) != '$(VERSION)' else '(already at version)'); open(p,'w').write(re.sub(r'^ version: \"[^\"]*\"', ' version: \"$(VERSION)\"', s, count=1, flags=re.M))"
@git add wails.json build/config.yml
@git diff --cached --quiet && echo "versions already at $(VERSION), skipping bump commit" || git commit -m "chore(release): bump version to $(VERSION)"
@echo "Generating release notes for $(VERSION)..." @echo "Generating release notes for $(VERSION)..."
@git tag "$(VERSION)" HEAD && \ @git tag "$(VERSION)" HEAD && \
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \ (git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
+32 -6
View File
@@ -8,6 +8,14 @@ This has been built with the official Wails Svelte-TS template.
To run as is, please feel free to download a binary from the releases page. To run as is, please feel free to download a binary from the releases page.
> **To run:** install the WebKitGTK 6 runtime first — it is not preinstalled
> on most distros, and the app will not start without it
> (`error while loading shared libraries: libwebkitgtk-6.0.so.4`).
> The release tarball's `install_linux.sh` installs it automatically
> (Arch, Debian/Ubuntu, Fedora, Void, OpenMandriva). Manual fallback —
> Arch: `sudo pacman -S webkitgtk-6.0` ·
> Debian/Ubuntu: `sudo apt install libwebkitgtk-6.0-4`.
If you are getting too many errors due to api usage, please build from source. If you are getting too many errors due to api usage, please build from source.
## Build from Source ## Build from Source
@@ -29,15 +37,33 @@ Simkl: [Simkl Developer](https://simkl.com/settings/developer/)
Once you have the IDs, Keys, and Secrets create an environment.go file based on the environment.go.example and fill in the fields. Once you have the IDs, Keys, and Secrets create an environment.go file based on the environment.go.example and fill in the fields.
### Install Wails and Dependencies ### Install Wails and Dependencies
Please follow the instructions [here](https://wails.io/docs/gettingstarted/installation) to get Wails up and running and follow the instructions below. Please follow the instructions [here](https://v3.wails.io/getting-started/your-first-app/) to get Wails v3 up and running and follow the instructions below.
System packages for building (the WebKitGTK 6 runtime above, plus
compilers and headers) — Arch: `sudo pacman -S base-devel gtk4 webkitgtk-6.0 go nodejs npm` ·
Debian/Ubuntu: `sudo apt install build-essential pkg-config libgtk-4-dev libwebkitgtk-6.0-dev golang nodejs npm`,
then install the pinned CLI with `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.20`.
> **Versioning note:** `1.99.x` is the v3-migration beta series — Wails v3
> underneath, Svelte 4/Vite 4 toolchain refresh still pending. `2.0.0` is
> reserved for the finished article. See `docs/V3_MIGRATION.md`.
## Live Development ## Live Development
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development To run in live development mode, run `make dev` in the project directory (it runs `wails3 dev`). This starts a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser server on http://localhost:5173 with very fast hot reload of your frontend changes, alongside the desktop app window.
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools. ## Migrating from v2
The `wailsv3` branch moves AniTrack from Wails `v2``v3` and from `99designs/keyring``zalando/go-keyring`. Both versions can coexist: on Linux, `v2` tokens live in an `AniTrack` Secret Service collection while `v3` uses the `login` collection, so one does not overwrite the other. After you log into AniList / MAL / Simkl once on `v3`, the `v2` entries become stale but harmless orphans — leave them if you still run both versions, or clear the old collection when youve fully moved over. See `docs/V3_MIGRATION.md` for the full migration summary and next steps (including planned Android support).
## Building ## Building
To build a redistributable, production mode package, use `wails build --clean`. To build a redistributable, production mode package, use `make build`. The binary lands at `build/bin/AniTrack`.
## Updates
Desktop Linux releases self-update: the app checks for a newer release at
startup and offers it in-app (signed, verified before install). Only the
binary updates itself — icons and the `.desktop` file still come from the
release tarball. Details in `docs/V3_MIGRATION.md: Self-updates`.
+31 -67
View File
@@ -11,41 +11,17 @@ import (
"net/http" "net/http"
"sync" "sync"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var simklJwt SimklJWT var simklJwt SimklJWT
var simklRing keyring.Keyring // simklKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
func init() { const simklKeyringService = "AniTrack"
var err error
simklRing, err = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
if err != nil {
log.Printf("simkl: secure storage unavailable: %s", err)
}
}
func simklRingReady() bool {
if simklRing == nil {
log.Println("simkl: secure storage unavailable")
return false
}
return true
}
func simklRingSet(key string, data []byte) error { func simklRingSet(key string, data []byte) error {
if !simklRingReady() { if err := keyring.Set(simklKeyringService, key, string(data)); err != nil {
return errors.New("simkl: secure storage unavailable")
}
if err := simklRing.Set(keyring.Item{Key: key, Data: data}); err != nil {
log.Printf("simkl: save %s failed: %s", key, err) log.Printf("simkl: save %s failed: %s", key, err)
return err return err
} }
@@ -56,18 +32,15 @@ var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
func (a *App) CheckIfSimklLoggedIn() bool { func (a *App) CheckIfSimklLoggedIn() bool {
if (SimklJWT{} == simklJwt) { if (SimklJWT{} == simklJwt) {
if !simklRingReady() { tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
return false accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
} scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType") if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken) == 0 {
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
return false return false
} else { } else {
simklJwt.TokenType = string(tokenType.Data) simklJwt.TokenType = tokenType
simklJwt.AccessToken = string(accessToken.Data) simklJwt.AccessToken = accessToken
simklJwt.Scope = string(scope.Data) simklJwt.Scope = scope
return true return true
} }
} else { } else {
@@ -77,25 +50,24 @@ func (a *App) CheckIfSimklLoggedIn() bool {
func (a *App) SimklLogin() { func (a *App) SimklLogin() {
if !a.CheckIfSimklLoggedIn() { if !a.CheckIfSimklLoggedIn() {
if !simklRingReady() { tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
log.Println("simkl: cannot check login, secure storage unavailable") accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
return scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
} if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken) == 0 {
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl) if err := a.app.Browser.OpenURL(getSimklCodeUrl); err != nil {
log.Printf("simkl: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
a.handleSimklCallback(serverDone) a.handleSimklCallback(serverDone)
serverDone.Wait() serverDone.Wait()
} else { } else {
simklJwt.TokenType = string(tokenType.Data) simklJwt.TokenType = tokenType
simklJwt.AccessToken = string(accessToken.Data) simklJwt.AccessToken = accessToken
simklJwt.Scope = string(scope.Data) simklJwt.Scope = scope
} }
} }
} }
@@ -117,17 +89,13 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
_ = simklRingSet("SimklTokenType", []byte(simklJwt.TokenType)) _ = simklRingSet("SimklTokenType", []byte(simklJwt.TokenType))
_ = simklRingSet("SimklAccessToken", []byte(simklJwt.AccessToken)) _ = simklRingSet("SimklAccessToken", []byte(simklJwt.AccessToken))
_ = simklRingSet("SimklScope", []byte(simklJwt.Scope)) _ = simklRingSet("SimklScope", []byte(simklJwt.Scope))
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{ a.app.Dialog.Info().
Title: "Simkl Authorization", SetTitle("Simkl Authorization").
Message: "It is now safe to close your browser tab", SetMessage("It is now safe to close your browser tab").
}) Show()
if err != nil {
log.Println(err)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
simklCancel() simklCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -243,13 +211,9 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
func (a *App) LogoutSimkl() string { func (a *App) LogoutSimkl() string {
if (SimklJWT{} != simklJwt) { if (SimklJWT{} != simklJwt) {
if !simklRingReady() { tokenTypeErr := keyring.Delete(simklKeyringService, "SimklTokenType")
simklJwt = SimklJWT{} accessTokenErr := keyring.Delete(simklKeyringService, "SimklAccessToken")
return "Simkl Logged Out Successfully" scopeErr := keyring.Delete(simklKeyringService, "SimklScope")
}
tokenTypeErr := simklRing.Remove("SimklTokenType")
accessTokenErr := simklRing.Remove("SimklAccessToken")
scopeErr := simklRing.Remove("SimklScope")
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil { if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil {
log.Printf("simkl: logout cleanup failed (type=%v access=%v scope=%v)", tokenTypeErr, accessTokenErr, scopeErr) log.Printf("simkl: logout cleanup failed (type=%v access=%v scope=%v)", tokenTypeErr, accessTokenErr, scopeErr)
+30
View File
@@ -0,0 +1,30 @@
version: '3'
vars:
APP_NAME: "AniTrack"
# build/bin keeps the v2 output path so the release packaging is unchanged.
BIN_DIR: "build/bin"
PACKAGE_MANAGER: '{{.PACKAGE_MANAGER | default "npm"}}'
VITE_PORT: '{{.WAILS_VITE_PORT | default 5173}}'
# Target OS for build/package/run. Defaults to the host OS.
GOOS: '{{.GOOS | default OS}}'
includes:
common: ./build/Taskfile.yml
linux: ./build/linux/Taskfile.yml
tasks:
build:
summary: Builds the application
cmds:
- task: "{{.GOOS}}:build"
run:
summary: Runs the application
cmds:
- task: "{{.GOOS}}:run"
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
+23 -32
View File
@@ -1,58 +1,49 @@
package main package main
import ( import (
"context"
_ "embed" _ "embed"
"log"
"strings" "strings"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/wailsapp/wails/v3/pkg/application"
) )
//go:embed wails.json //go:embed wails.json
var wailsJSON string var wailsJSON string
var wailsContext *context.Context
// App struct // App struct
type App struct { type App struct {
ctx context.Context app *application.App
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { func NewApp(app *application.App) *App {
return &App{} return &App{app: app}
} }
// startup is called when the app starts. The context is saved // appVersion reads the product version from wails.json.
// so we can call the runtime methods func appVersion() string {
func (a *App) startup(ctx context.Context) { return gjson.Get(wailsJSON, "info.productVersion").String()
version := gjson.Get(wailsJSON, "info.productVersion")
wailsContext = &ctx
runtime.WindowSetTitle(ctx, "AniTrack "+version.String())
//runtime.WindowMaximise(ctx)
} }
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { // appTitle is the window title: "AniTrack <version>".
var secondInstanceArgs = secondInstanceData.Args func appTitle() string {
return "AniTrack " + appVersion()
}
println("user opened second instance", strings.Join(secondInstanceData.Args, ",")) func (a *App) onSecondInstanceLaunch(data application.SecondInstanceData) {
println("user opened second from", secondInstanceData.WorkingDirectory) println("user opened second instance", strings.Join(data.Args, ","))
runtime.WindowUnminimise(*wailsContext) println("user opened second from", data.WorkingDir)
runtime.Show(*wailsContext) if w := a.app.Window.Current(); w != nil {
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs) w.Restore()
w.Focus()
}
go a.app.Event.Emit("launchArgs", data.Args)
} }
func (a *App) ShowVersion() { func (a *App) ShowVersion() {
version := gjson.Get(wailsJSON, "info.productVersion") a.app.Dialog.Info().
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{ SetTitle("Version").
Title: "Version", SetMessage("AniTrack Version: " + appVersion()).
Message: "AniTrack Version: " + version.String(), Show()
})
if err != nil {
log.Println(err)
}
} }
+398
View File
@@ -0,0 +1,398 @@
version: '3'
tasks:
go:mod:tidy:
summary: Runs `go mod tidy`
internal: true
# Universal/multi-arch builds invoke this concurrently from parallel deps;
# two `go mod tidy` processes racing on go.mod can corrupt it (#4637).
run: once
cmds:
- go mod tidy
install:frontend:deps:
summary: Install frontend dependencies
run: once
cmds:
- task: install:frontend:deps:{{.PACKAGE_MANAGER}}
install:frontend:deps:npm:
dir: frontend
sources:
- package.json
- package-lock.json
generates:
- node_modules
preconditions:
- sh: npm version
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
cmds:
- npm install
install:frontend:deps:bun:
dir: frontend
sources:
- package.json
- bun.lock
- bun.lockb
generates:
- node_modules
preconditions:
- sh: bun --version
msg: "bun not found"
cmds:
- bun install
install:frontend:deps:pnpm:
dir: frontend
sources:
- package.json
- pnpm-lock.yaml
generates:
- node_modules
preconditions:
- sh: pnpm --version
msg: "pnpm not found"
cmds:
- pnpm install
install:frontend:deps:yarn:
dir: frontend
sources:
- package.json
- yarn.lock
status:
- test -d node_modules || test -f .pnp.cjs
preconditions:
- sh: yarn --version
msg: "yarn not found"
cmds:
- yarn install
build:frontend:
label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}})
summary: Build the frontend project
# darwin:build:universal runs its per-arch builds as parallel deps, each of
# which depends on this task. Without run:once the two executions race:
# one regenerates frontend/bindings (-clean deletes it first) while the
# other's bundler is reading it, failing intermittently with
# 'Could not resolve "./bindings/<pkg>"' (#4637).
run: once
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:
- task: install:frontend:deps
- task: generate:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
OBFUSCATED:
ref: .OBFUSCATED
cmds:
- task: frontend:run
vars:
SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
env:
PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
frontend:run:
summary: Run a frontend script with selected runner
cmds:
- task: frontend:run:{{.PACKAGE_MANAGER}}
vars:
SCRIPT: "{{.SCRIPT}}"
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:npm:
dir: frontend
cmds:
- npm run {{.SCRIPT}} -q
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:yarn:
dir: frontend
cmds:
- yarn {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:pnpm:
dir: frontend
cmds:
- pnpm run {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:bun:
dir: frontend
cmds:
- bun run {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:vendor:puppertino:
summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
sources:
- frontend/public/puppertino/puppertino.css
generates:
- frontend/public/puppertino/puppertino.css
cmds:
- |
set -euo pipefail
mkdir -p frontend/public/puppertino
# If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
if [ ! -f frontend/public/puppertino/puppertino.css ]; then
echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
else
echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
fi
else
echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
fi
# Ensure index.html includes Puppertino CSS and button classes
INDEX_HTML=frontend/index.html
if [ -f "$INDEX_HTML" ]; then
if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
# Insert Puppertino link tag after style.css link
awk '
/href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
fi
# Replace default .btn with Puppertino primary button classes if present
sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
fi
generate:bindings:
summary: Generates bindings for the frontend
run: once
deps:
- task: go:mod:tidy
sources:
- "**/*.[jt]s"
- exclude: frontend/**/*
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
run: once
dir: build
sources:
- "appicon.png"
- "appicon.icon"
generates:
- "darwin/icons.icns"
- "windows/icon.ico"
cmds:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- task: frontend:dev:{{.PACKAGE_MANAGER}}
frontend:dev:npm:
dir: frontend
cmds:
- npm run dev -- --port {{.VITE_PORT}} --strictPort
frontend:dev:yarn:
dir: frontend
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
frontend:dev:pnpm:
dir: frontend
cmds:
- pnpm dev --port {{.VITE_PORT}} --strictPort
frontend:dev:bun:
dir: frontend
cmds:
- bun run dev --port {{.VITE_PORT}} --strictPort
update:build-assets:
summary: Updates the build assets
dir: build
cmds:
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
build:server:
summary: Builds the application in server mode (no GUI, HTTP server only)
desc: |
Builds a production server binary by default: -tags server,production,
-trimpath and a stripped binary, mirroring the desktop `build` task.
Server mode runs as a pure HTTP server without native GUI dependencies.
Usage: task build:server [DEV=true] [OBFUSCATED=true] [EXTRA_TAGS=tag1,tag2]
DEV=true development server (-tags server, no strip, inlining kept)
OBFUSCATED=true obfuscated build via garble (requires garble installed)
EXTRA_TAGS additional comma-separated build tags
deps:
- task: build:frontend
vars:
DEV:
ref: .DEV
BUILD_FLAGS:
ref: .BUILD_FLAGS
OBFUSCATED:
ref: .OBFUSCATED
preconditions:
- sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
cmds:
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
vars:
BUILD_FLAGS: '-tags server{{if eq .DEV "true"}}{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -buildvcs=false -gcflags=all="-l"{{else}},production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
run:server:
summary: Builds and runs a development server (DEV=true)
deps:
- task: build:server
vars:
DEV: "true"
cmds:
- '"./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
build:docker:
summary: Builds a Docker image for server mode deployment
desc: |
Creates a minimal Docker image containing the production server binary.
Defaults to a pure-Go static binary on a distroless/static base. The
production frontend is built first so the embedded assets are current.
Usage: task build:docker [TAG=myapp:latest] [CGO_ENABLED=1] [GO_IMAGE=...] [RUNTIME_IMAGE=...]
For CGO apps, set CGO_ENABLED=1 with libc-compatible builder/runtime images, e.g.:
task build:docker CGO_ENABLED=1 GO_IMAGE=golang:bookworm RUNTIME_IMAGE=gcr.io/distroless/base-debian12
deps:
# Build the production frontend so frontend/dist (embedded by the Go build
# inside the image) is present and current in the Docker build context.
# Pass the server,production tags so binding generation analyses the same
# build the Docker image compiles, not the default-tag build.
- task: build:frontend
vars:
BUILD_FLAGS: "-tags server,production"
cmds:
- >-
docker build
--build-arg CGO_ENABLED={{.CGO_ENABLED | default "0"}}
--build-arg GO_IMAGE={{.GO_IMAGE | default "golang:alpine"}}
--build-arg RUNTIME_IMAGE={{.RUNTIME_IMAGE | default "gcr.io/distroless/static-debian12"}}
-t {{.TAG | default (printf "%s:latest" .APP_NAME)}}
-f build/docker/Dockerfile.server .
vars:
TAG: "{{.TAG}}"
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
- sh: test -f build/docker/Dockerfile.server
msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
run:docker:
summary: Builds and runs the Docker image
desc: |
Builds the Docker image and runs it, exposing port 8080.
Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
Note: The internal container port is always 8080. The PORT variable
only changes the host port mapping. Ensure your app uses port 8080
or modify the Dockerfile to match your ServerOptions.Port setting.
deps:
- task: build:docker
vars:
TAG:
ref: .TAG
cmds:
- docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
vars:
TAG: "{{.TAG}}"
PORT: "{{.PORT}}"
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
desc: |
Builds the Docker image needed for cross-compiling to any platform.
Run this once to enable cross-platform builds from any OS.
cmds:
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
ios:device:list:
summary: Lists connected iOS devices (UDIDs)
cmds:
- xcrun xcdevice list
ios:run:device:
summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
vars:
PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
SCHEME: '{{.SCHEME}}' # e.g., ios.dev
CONFIG: '{{.CONFIG | default "Debug"}}'
DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
UDID: '{{.UDID}}' # from `task ios:device:list`
BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
preconditions:
- sh: xcrun -f xcodebuild
msg: "xcodebuild not found. Please install Xcode."
- sh: xcrun -f devicectl
msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
- sh: test -n '{{.PROJECT}}'
msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
- sh: test -n '{{.SCHEME}}'
msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
- sh: test -n '{{.UDID}}'
msg: "Set UDID to your device UDID (see: task ios:device:list)."
- sh: test -n '{{.BUNDLE_ID}}'
msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
cmds:
- |
set -euo pipefail
echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
XCB_ARGS=(
-project "{{.PROJECT}}"
-scheme "{{.SCHEME}}"
-configuration "{{.CONFIG}}"
-destination "id={{.UDID}}"
-derivedDataPath "{{.DERIVED}}"
-allowProvisioningUpdates
-allowProvisioningDeviceRegistration
)
# Optionally inject signing identifiers if provided
if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
# If xcpretty isn't installed, run without it
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
xcodebuild "${XCB_ARGS[@]}" build
fi
# Find built .app
APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
exit 1
fi
echo "Installing: $APP_PATH"
xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
echo "Launching: {{.BUNDLE_ID}}"
xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
+42
View File
@@ -0,0 +1,42 @@
# Wails v3 project configuration.
# NOTE: `make release` bumps `info.version` below alongside wails.json.
# Never touch the top-level `version: '3'` — that is the config file schema,
# not the app version.
version: '3'
info:
companyName: "John O'Keefe"
productName: "AniTrack"
productIdentifier: "com.linuxhg.anitrack"
description: "Track anime watchlists across AniList, MyAnimeList, and Simkl"
copyright: "John O'Keefe"
version: "1.99.1"
# Dev mode configuration
dev_mode:
root_path: .
log_level: warn
debounce: 1000
ignore:
dir:
- .git
- node_modules
- frontend
- bin
file:
- .DS_Store
- .gitignore
- .gitkeep
- "*_test.go"
watched_extension:
- "*.go"
- "*.js"
- "*.ts"
git_ignore: true
executes:
- cmd: wails3 build DEV=true
type: blocking
- cmd: wails3 task common:dev:frontend
type: background
- cmd: wails3 task run
type: primary
-68
View File
@@ -1,68 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
-68
View File
@@ -1,68 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true />
</dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>
+94
View File
@@ -1,4 +1,98 @@
#!/bin/bash #!/bin/bash
set -u
# Step 0: WebKitGTK 6 runtime ...................................................
# The release binary dynamically links libwebkitgtk-6.0 and libgtk-4, which are
# not preinstalled on most distros and are not shipped in the tarball. Install
# them first so the app can start; abort before copying anything otherwise.
#
# OS_RELEASE_FILE override exists for testing the distro mapping only.
print_manual_instructions() {
cat >&2 <<'EOF'
Install the WebKitGTK 6 runtime for your distribution, then re-run this script:
Arch / Manjaro / EndeavourOS: sudo pacman -S webkitgtk-6.0
Debian / Ubuntu / Mint / Pop: sudo apt install libwebkitgtk-6.0-4
Fedora / Nobara: sudo dnf install webkitgtk6.0
OpenMandriva: sudo dnf install lib64webkit2gtk6.0
Void (glibc): sudo xbps-install -S libwebkitgtk60
Without it the app fails at launch: error while loading shared libraries: libwebkitgtk-6.0.so.4
EOF
}
install_webkit_runtime() {
# 0a. musl guard: the prebuilt binary is glibc-linked and cannot run on
# musl systems (Alpine, Void-musl). No package fixes that — refuse loudly.
if ldd --version 2>&1 | grep -qi musl; then
echo "ERROR: AniTrack ships a glibc-linked binary, but this system uses musl libc." >&2
echo "The prebuilt release cannot run here. Build from source instead (see README)." >&2
return 1
fi
# 0b. Every package manager below needs privilege.
local sudo_cmd=""
if [ "$(id -u)" -ne 0 ]; then
if ! command -v sudo >/dev/null 2>&1; then
echo "ERROR: root privileges are required to install system packages," >&2
echo "but sudo is not available. Install the WebKitGTK 6 runtime manually:" >&2
print_manual_instructions
return 1
fi
sudo_cmd="sudo"
fi
# 0c. Distro mapping. ID_LIKE catches derivatives (Mint/Pop -> ubuntu,
# Manjaro/EndeavourOS -> arch, Nobara -> fedora) without enumerating them.
local os_release="${OS_RELEASE_FILE:-/etc/os-release}"
local dist_id="" dist_like=""
if [ -r "$os_release" ]; then
# shellcheck disable=SC1090
. "$os_release"
dist_id="${ID:-}"
dist_like="${ID_LIKE:-}"
fi
need_cmd() {
command -v "$1" >/dev/null 2>&1
}
local ids=" ${dist_id} ${dist_like} "
case "$ids" in
*" arch "*|*" manjaro "*|*" endeavouros "*|*" garuda "*|*" cachyos "*)
need_cmd pacman || { echo "ERROR: pacman not found on Arch-family system." >&2; return 1; }
$sudo_cmd pacman -S --needed --noconfirm webkitgtk-6.0 || return 1
;;
*" debian "*|*" ubuntu "*|*" pop "*|*" linuxmint "*)
need_cmd apt-get || { echo "ERROR: apt-get not found on Debian-family system." >&2; return 1; }
$sudo_cmd apt-get update && $sudo_cmd apt-get install -y libwebkitgtk-6.0-4 || return 1
;;
*" fedora "*|*" nobara "*)
need_cmd dnf || { echo "ERROR: dnf not found on Fedora-family system." >&2; return 1; }
$sudo_cmd dnf install -y webkitgtk6.0 || return 1
;;
*" openmandriva "*)
need_cmd dnf || { echo "ERROR: dnf not found on OpenMandriva system." >&2; return 1; }
$sudo_cmd dnf install -y lib64webkit2gtk6.0 || return 1
;;
*" void "*)
need_cmd xbps-install || { echo "ERROR: xbps-install not found on Void system." >&2; return 1; }
$sudo_cmd xbps-install -Sy libwebkitgtk60 || return 1
;;
*" alpine "*)
echo "ERROR: Alpine Linux uses musl libc, which the prebuilt glibc-linked binary cannot run on." >&2
echo "Build from source instead (see README)." >&2
return 1
;;
*)
echo "ERROR: unsupported or unrecognised distribution (ID='${dist_id}' ID_LIKE='${dist_like}')." >&2
print_manual_instructions
return 1
;;
esac
unset -f need_cmd
}
install_webkit_runtime || exit 1
# copy desktop file # copy desktop file
if [ ! -f "$HOME/.local/share/applications/AniTrack.desktop" ]; then if [ ! -f "$HOME/.local/share/applications/AniTrack.desktop" ]; then
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=AniTrack
Exec=AniTrack
Icon=AniTrack
Categories=Development;
Terminal=false
Keywords=wails
Version=1.0
StartupNotify=false
+222
View File
@@ -0,0 +1,222 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application for Linux
cmds:
# Linux requires CGO - use Docker when:
# 1. Cross-compiling from non-Linux, OR
# 2. No C compiler is available, OR
# 3. Target architecture differs from host architecture (cross-arch compilation)
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
OBFUSCATED: '{{.OBFUSCATED}}'
GARBLE_ARGS: '{{.GARBLE_ARGS}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Determine target architecture (defaults to host ARCH if not specified)
TARGET_ARCH: '{{.ARCH | default ARCH}}'
# Check if a C compiler is available (gcc or clang) — cross-platform via wails3 tool
HAS_CC:
sh: 'wails3 tool has "gcc|clang"'
build:native:
summary: Builds the application natively on Linux
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
OBFUSCATED:
ref: .OBFUSCATED
DEV:
ref: .DEV
# common:generate:icons intentionally unwired: it generates macOS/Windows
# icons from build/appicon.png, which this Linux-only project does not
# have (icons ship via build/icon + the release tarball instead).
- task: generate:dotdesktop
preconditions:
- sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
cmds:
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
vars:
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default ARCH}}'
build:docker:
summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
internal: true
deps:
- task: common:build:frontend
vars:
OBFUSCATED:
ref: .OBFUSCATED
- task: common:generate:icons
- task: generate:dotdesktop
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
- cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
platforms: [linux, darwin]
- mkdir -p {{.BIN_DIR}}
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Generate Docker volume mounts: Go module cache + go.mod replace directives
# Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
DOCKER_MOUNTS:
sh: 'wails3 tool docker-mounts'
package:
summary: Packages the application for Linux
deps:
- task: build
cmds:
- task: create:appimage
- task: create:deb
- task: create:rpm
- task: create:aur
create:appimage:
summary: Creates an AppImage
dir: build/linux/appimage
deps:
- task: build
- task: generate:dotdesktop
cmds:
- cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
- cp ../../appicon.png "{{.APP_NAME}}.png"
- wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
vars:
APP_NAME: '{{.APP_NAME}}'
APP_BINARY: '../../../bin/{{.APP_NAME}}'
ICON: '{{.APP_NAME}}.png'
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
OUTPUT_DIR: '../../../bin'
create:deb:
summary: Creates a deb package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:deb
create:rpm:
summary: Creates a rpm package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:rpm
create:aur:
summary: Creates a arch linux packager package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:aur
generate:deb:
summary: Creates a deb package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:rpm:
summary: Creates a rpm package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:aur:
summary: Creates a arch linux packager package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:dotdesktop:
summary: Generates a `.desktop` file
dir: build
cmds:
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
vars:
APP_NAME: '{{.APP_NAME}}'
EXEC: '{{.APP_NAME}}'
ICON: '{{.APP_NAME}}'
CATEGORIES: 'Development;'
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}'
sign:deb:
summary: Signs the DEB package
desc: |
Signs the .deb package with a PGP key.
Set PGP_KEY in the vars section to override the key configured globally via
`wails3 setup` (~/.config/wails/defaults.yaml).
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:deb
cmds:
# PGP_KEY is optional: if unset, `wails3 tool sign` falls back to the key
# configured globally via `wails3 setup`.
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}} {{if .SIGN_ROLE}}--role "{{.SIGN_ROLE}}"{{end}}
sign:rpm:
summary: Signs the RPM package
desc: |
Signs the .rpm package with a PGP key.
Set PGP_KEY in the vars section to override the key configured globally via
`wails3 setup` (~/.config/wails/defaults.yaml).
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:rpm
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}}
sign:packages:
summary: Signs all Linux packages (DEB and RPM)
desc: |
Signs both .deb and .rpm packages with a PGP key.
Set PGP_KEY in the vars section to override the key configured globally via
`wails3 setup` (~/.config/wails/defaults.yaml).
Password is retrieved from system keychain (run: wails3 setup signing)
cmds:
- task: sign:deb
- task: sign:rpm
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-15
View File
@@ -1,15 +0,0 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}
-114
View File
@@ -1,114 +0,0 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd
-249
View File
@@ -1,249 +0,0 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
+66
View File
@@ -0,0 +1,66 @@
# AniTrack v3 Migration — What Changed and What's Next
This document summarizes the `wailsv3` branch (Wails `v2.15.0``v3.0.0-beta.20`, desktop-only) and the plan going forward. Its written for a future me and for anyone following the migration.
## What we did (desktop, no mobile yet)
**Goal:** get the existing app running on Wails v3 with the same structure and behavior — no rewrites of AniList / MAL / Simkl logic, no mobile, no stores.
**Backend — single `App` service preserved:**
- `main.go`: `wails.Run(options.App{ Bind: app })``application.New( Services: [app] )` + `Window.NewWithOptions(...)` + `app.Run()`. Same window (1024×768, title `AniTrack <version>`, RGBA background, Linux `ProgramName`/`Icon`/`GpuPolicyNever`), same `SingleInstance` lock/ID and `onSecondInstanceLaunch` (now `SecondInstanceData{ Args, WorkingDir }` + `Window.Current().Restore/Focus` + `Event.Emit("launchArgs")`).
- `app.go`: `App` now holds `*application.App` instead of a `context`. `startup(ctx)` / global `wailsContext` removed; version title moved into the window options. `ShowVersion` and the three OAuth callbacks now use `app.Dialog.Info()` / `app.Browser.OpenURL()` (errors logged, not ignored).
- `AniListUserFunctions.go` / `MALUserFunctions.go` / `SimklUserFunctions.go`: 11 runtime calls migrated (`BrowserOpenURL` ×3, `MessageDialog` ×3/4, `EventsEmit` ×1, window calls) to `v3` managers (`Browser`, `Dialog`, `Event`, `Window`). OAuth servers on `:6734/callback` unchanged. Per-file key constants unchanged.
**Secrets — `99designs/keyring` → `zalando/go-keyring`:**
- `go.mod`: `wails/v2` + `99designs v1.2.2` dropped (stale `replace` removed), `wails/v3 v3.0.0-beta.20` (pinned to the local `wails3` CLI) + `zalando v0.2.8` added — what `v3` itself depends on.
- Same service name `AniTrack` and same 11 key names (`anilist*`, `MyAnimeList*`, `Simkl*`), so intent is the same wallet. On Linux the underlying collection differs (`AniTrack` vs `login`) — see “Stale keys” below — so first `v3` run requires re-logging into the 3 services once.
**Frontend — bindings + runtime:**
- `wails3 generate bindings``frontend/bindings/AniTrack/*.ts` (1 service, 29 methods, 25 models), verified byte-deterministic. `frontend/wailsjs/` removed. 10 Svelte files updated: `wailsjs/go/main/App``bindings/AniTrack` (`App.*` call prefixes), `WebsiteLink.svelte``Browser.OpenURL`, `AvatarMenu.svelte``Application.Quit`, both from `@wailsio/runtime` `3.0.0-beta.20`.
- `frontend/vite.config.ts`: added `@wailsio/runtime` vite plugin (`wails('./bindings')`) + `server.host/port/strictPort` (5173). `frontend/package.json`: added `build:dev` (`vite build --minify false --mode development`, required by the `v3` dev taskflow) and `@wailsio/runtime` dep.
**Build + CI:**
- `Taskfile.yml` + `build/Taskfile.yml` (stock `common`) + `build/linux/Taskfile.yml` (stock minus `common:generate:icons` — macOS/Windows icon generation targets `build/appicon.png` which this Linux-only project doesnt have) + `build/config.yml` (AniTrack metadata, `Version 1.6.8`). Root output stays `build/bin/AniTrack` so release packaging is untouched. `wails.json``v3` `frontend` block, `info.productVersion` retained.
- `Makefile`: `wails dev/build -tags webkit2_41``wails3 dev -port 5173` / `wails3 build` (no tags; `v3` defaults to `GTK4/WebKitGTK 6.0`, same `2.52.x` engine generation). Added `.task/` to `.gitignore`. Removed `build/darwin/`, `build/windows/`, `frontend/package.json.md5`. `make release` bumps both `wails.json` and `build/config.yml:info.version` (TEMP until `wails.json` is retired).
- `.gitea/workflows/release.yml`: Wails CLI `wails@v2.15.0``wails3@v3.0.0-beta.20` (path + cache key), apt `libgtk-3-dev/libwebkit2gtk-4.1-dev``libgtk-4-dev/libwebkitgtk-6.0-dev`, `make build` unchanged. Go/npm/go-build/Wails-CLI caches remain valid (npm cache now resolves since `frontend/package-lock.json` is committed).
**Verification:** `go vet` clean, `wails3 build` green (`build/bin/AniTrack` ~13 MB), `wails3 dev` boots end-to-end (bindings regen → `build:dev` → vite on `:5173``Connected to frontend dev server``AniTrack 1.6.8` window), second instance exits 0 via the lock. `svelte-check` was already red on `main` (`wailsjs/go/models.ts` anonymous structs) and remains so — not introduced by the migration.
**Commits on `wailsv3`:** `chore(wailsv3): migrate Go backend…`, `chore(wailsv3): migrate frontend…`, `chore(wailsv3): add v3 Taskfile…`, `ci(release): build releases with the Wails v3 toolchain`, plus `chore(wailsv3): v3 conformance pass…`.
## Stale keys — what a v2 user will see
On Linux, `v2` tokens lived in an `AniTrack` Secret Service collection as labeled JSON items; `v3` (zalando) uses the `login` collection with `service`/`username` attributes. Same OS keyring, different collections — so after migrating, the old `AniTrack` collection is orphaned.
In practice: running both versions side-by-side works fine — each reads its own collection, neither overwrites the other. If you fully move to `v3`, just log into AniList / MAL / Simkl once there; the `v2` entries become stale but harmless (11 small items). See the notice in `README.md: Migrating from v2` — no automatic deletion, on purpose.
## Self-updates (desktop Linux, 1.99.x)
Releases self-update in place via the Wails v3 updater (`app.Updater`): a
custom Gitea provider (`updater/gitea` — Wails ships none for Gitea) checks
the Gitea releases API, and CI publishes a signed bare-binary asset
(`AniTrack-linux-amd64` + `.sha512`/`.sig` sidecars from
`wails3 updater sign`) next to the user tarball. Verification is fail-closed;
a release missing any of the three files is skipped. Gated to desktop
production builds (`application.System.IsDesktop()` + `-tags production`) —
mobile stays on Obtainium, dev builds never check. The startup check is
headless; the builtin window opens only when an update is found.
`ANITRACK_UPDATER_CHANNEL=beta` includes `-rc` pre-releases (testing only).
Only the binary self-swaps; icons/`.desktop` still come from the tarball's
`install_linux.sh`. Signing: public half `updater.pub` is embedded; the
private key lives in the password manager + the `UPDATER_SIGNING_KEY` CI
secret, never in the repo (see `.gitignore`).
## Future plans on v3
**Not yet done — staged after the desktop migration, in order** (each with its home branch, so nothing lands in the wrong place):
1. **Stabilize desktop `v3`:** real-world dogfooding of `main` (logins, watchlist sync, error modals). Merged to `main` as the `1.99.x` beta series while Wails `v3` is still beta upstream.
2. **Frontend toolchain refresh (on `wailsv3`, before the merge):** Svelte 4 → 5, Vite 4 → 8, `vite-plugin-svelte 2 → 7`, Tailwind 3 → 4 — majors deferred intentionally; they pair naturally with `v3`s Svelte 5 templates and touch the same desktop files, so no separate branch.
3. **Android (short-lived feature branch off `wailsv3`, e.g. `wailsv3-android`, merged back):** personal sideload, no Play/official F-Droid. Per-file `//go:build android` shims — `zalando``Android.Secure*` (`EncryptedSharedPreferences`), and `localhost:6734` OAuth callbacks → deep-link `anitrack://callback` + intent filter; start with one provider (AniList) to prove the pattern. Responsive / safe-area polish. Distribution via `adb install` + GitHub Releases + Obtainium. Kept off `wailsv3` proper so the mobile scaffolding (`build/android/`, manifests, gradle files) doesn't pollute the desktop-to-`main` merge.
4. **CI follow-ups (wherever the work they support lives):** none required for desktop; the current 4 caches + the fixed runner cache backend already bring releases from ~12 min to ~3 min. Builder image only if the 2 min apt floor becomes annoying.
## Branch map
- `main` — v3 desktop, ships `1.99.x` beta-series releases.
- `wailsv3` — retained as an alias tracking `main` for now.
+131
View File
@@ -0,0 +1,131 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* App struct
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export function AniListBrowse(page: number, perPage: number, id: number, isAdult: boolean, search: string, format: string[] | null, status: string, countryOfOrigin: string, source: string, season: string, seasonYear: number, year: string, onList: boolean, yearLesser: number, yearGreater: number, episodeLesser: number, episodeGreater: number, durationLesser: number, durationGreater: number, chapterLesser: number, chapterGreater: number, volumeLesser: number, volumeGreater: number, licensedBy: number[] | null, isLicensed: boolean, genres: string[] | null, excludedGenres: string[] | null, tags: string[] | null, excludedTags: string[] | null, minimumTagRank: number, sort: string[] | null): $CancellablePromise<$models.AniListCurrentUserWatchList> {
return $Call.ByID(509958107, page, perPage, id, isAdult, search, format, status, countryOfOrigin, source, season, seasonYear, year, onList, yearLesser, yearGreater, episodeLesser, episodeGreater, durationLesser, durationGreater, chapterLesser, chapterGreater, volumeLesser, volumeGreater, licensedBy, isLicensed, genres, excludedGenres, tags, excludedTags, minimumTagRank, sort);
}
export function AniListDeleteEntry(mediaListId: number): $CancellablePromise<$models.DeleteAniListReturn> {
return $Call.ByID(814044062, mediaListId);
}
export function AniListLogin(): $CancellablePromise<void> {
return $Call.ByID(1289244998);
}
export function AniListSearch(query: string): $CancellablePromise<any> {
return $Call.ByID(535361453, query);
}
export function AniListUpdateEntry(updateBody: $models.AniListUpdateVariables): $CancellablePromise<$models.AniListGetSingleAnime> {
return $Call.ByID(1288135876, updateBody);
}
export function CheckIfAniListLoggedIn(): $CancellablePromise<boolean> {
return $Call.ByID(3851355);
}
export function CheckIfMyAnimeListLoggedIn(): $CancellablePromise<boolean> {
return $Call.ByID(3447028267);
}
export function CheckIfSimklLoggedIn(): $CancellablePromise<boolean> {
return $Call.ByID(2483871029);
}
export function DeleteMyAnimeListEntry(id: number): $CancellablePromise<boolean> {
return $Call.ByID(3393515640, id);
}
export function GetAniListItem(aniId: number, login: boolean): $CancellablePromise<$models.AniListGetSingleAnime> {
return $Call.ByID(2336493728, aniId, login);
}
export function GetAniListLoggedInUser(): $CancellablePromise<$models.AniListUser> {
return $Call.ByID(448499589);
}
export function GetAniListUserWatchingList(page: number, perPage: number, sort: string): $CancellablePromise<$models.AniListCurrentUserWatchList> {
return $Call.ByID(3978330877, page, perPage, sort);
}
export function GetMyAnimeList(count: number): $CancellablePromise<$models.MALWatchlist> {
return $Call.ByID(1771759135, count);
}
export function GetMyAnimeListAnime(id: number): $CancellablePromise<$models.MALAnime> {
return $Call.ByID(1368244377, id);
}
export function GetMyAnimeListLoggedInUser(): $CancellablePromise<$models.MyAnimeListUser> {
return $Call.ByID(768446973);
}
export function GetSimklLoggedInUser(): $CancellablePromise<$models.SimklUser> {
return $Call.ByID(2872164207);
}
export function LogoutAniList(): $CancellablePromise<string> {
return $Call.ByID(1911309547);
}
export function LogoutMyAnimeList(): $CancellablePromise<string> {
return $Call.ByID(2568421227);
}
export function LogoutSimkl(): $CancellablePromise<string> {
return $Call.ByID(734133957);
}
export function MyAnimeListLogin(): $CancellablePromise<void> {
return $Call.ByID(439153478);
}
export function MyAnimeListUpdate(anime: $models.MALAnime, update: $models.MALUploadStatus): $CancellablePromise<$models.MalListStatus> {
return $Call.ByID(2544871568, anime, update);
}
export function ShowVersion(): $CancellablePromise<void> {
return $Call.ByID(4089383544);
}
export function SimklGetUserWatchlist(): $CancellablePromise<$models.SimklWatchListType> {
return $Call.ByID(3479741957);
}
export function SimklLogin(): $CancellablePromise<void> {
return $Call.ByID(4146248);
}
export function SimklSearch(aniListAnime: $models.MediaList): $CancellablePromise<$models.SimklAnime> {
return $Call.ByID(3130411731, aniListAnime);
}
export function SimklSyncEpisodes(anime: $models.SimklAnime, progress: number): $CancellablePromise<$models.SimklAnime> {
return $Call.ByID(3607297770, anime, progress);
}
export function SimklSyncRating(anime: $models.SimklAnime, rating: number): $CancellablePromise<$models.SimklAnime> {
return $Call.ByID(2909509273, anime, rating);
}
export function SimklSyncRemove(anime: $models.SimklAnime): $CancellablePromise<boolean> {
return $Call.ByID(1794667404, anime);
}
export function SimklSyncStatus(anime: $models.SimklAnime, status: string): $CancellablePromise<$models.SimklAnime> {
return $Call.ByID(2561714086, anime, status);
}
+35
View File
@@ -0,0 +1,35 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as App from "./app.js";
export {
App
};
export type {
AiringScheduleNode,
AniListCurrentUserWatchList,
AniListGetSingleAnime,
AniListUpdateVariables,
AniListUser,
AnimeStatistics,
CompletedAt,
DeleteAniListReturn,
FlexString,
MALAnime,
MALUploadStatus,
MALWatchlist,
MalListStatus,
Media,
MediaAiringSchedule,
MediaFuzzyDate,
MediaList,
MediaRelation,
MediaRelations,
MediaTitle,
MyAnimeListUser,
SimklAnime,
SimklUser,
SimklWatchListType,
StartedAt
} from "./models.js";
+240
View File
@@ -0,0 +1,240 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export interface AiringScheduleNode {
"id": number;
"airingAt": number;
"timeUntilAiring": number;
"episode": number;
"mediaId": number;
}
export interface AniListCurrentUserWatchList {
"data": {"Page": {"pageInfo": {"total": number, "perPage": number, "currentPage": number, "lastPage": number, "hasNextPage": boolean}, "mediaList": MediaList[] | null}};
}
export interface AniListGetSingleAnime {
"data": {"MediaList": MediaList};
}
export interface AniListUpdateVariables {
"mediaId": number;
"progress": number;
"status": string;
"score": number;
"repeat": number;
"notes": string;
"startedAt": StartedAt;
"completedAt": CompletedAt;
}
export interface AniListUser {
"data": {"Viewer": {"id": number, "name": string, "avatar": {"large": string, "medium": string}, "bannerImage": string, "siteUrl": string}};
}
export interface AnimeStatistics {
"num_items_watching": number;
"num_items_completed": number;
"num_items_on_hold": number;
"num_items_dropped": number;
"num_items_plan_to_watch": number;
"num_items": number;
"num_days_watched": number;
"num_days_watching": number;
"num_days_completed": number;
"num_days_on_hold": number;
"num_days_dropped": number;
"num_days": number;
"num_episodes": number;
"num_times_rewatched": number;
"mean_score": number;
}
export interface CompletedAt {
"year": number;
"month": number;
"day": number;
}
export interface DeleteAniListReturn {
"data": {"DeleteMediaListEntry": {"deleted": boolean}};
}
export type FlexString = string;
export interface MALAnime {
"id": number;
"title": string;
"main_picture": {"large": string, "medium": string};
"alternative_titles": {"synonyms": string[] | null, "en": string, "ja": string};
"start_date": string;
"end_date": string;
"synopsis": string;
"mean": number;
"rank": number;
"popularity": number;
"num_list_users": number;
"num_scoring_users": number;
"nsfw": string;
"genres": {"id": number, "name": string}[] | null;
"created_at": string;
"updated_at": string;
"media_type": string;
"status": string;
"my_list_status": MalListStatus;
"num_episodes": number;
"start_season": {"year": number, "season": string};
"broadcast": {"day_of_the_week": string, "start_time": string};
"source": string;
"average_episode_duration": number;
"rating": string;
"studios": {"id": number, "name": string}[] | null;
"pictures": {"large": string, "medium": string}[] | null;
"background": string;
"related_anime": {"node": MALAnime, "relation_type": string, "relation_type_formatted": string}[] | null;
"recommendations": {"node": MALAnime, "num_recommendations": number}[] | null;
"Statistics": {"num_list_users": number, "Status": {"watching": FlexString, "completed": FlexString, "on_hold": FlexString, "dropped": FlexString, "plan_to_watch": FlexString}};
}
export interface MALUploadStatus {
"status": string;
"is_rewatching": boolean;
"score": number;
"num_watched_episodes": number;
"num_times_rewatched": number;
"comments": string;
}
export interface MALWatchlist {
"data": {"node": {"id": number, "title": string, "main_picture": {"medium": string, "large": string}}, "list_status": {"status": string, "score": number, "num_episodes_watched": number, "is_rewatching": boolean, "updated_at": string, "start_date": string, "finish_date": string}}[] | null;
"paging": {"previous": string, "next": string};
}
export interface MalListStatus {
"status": string;
"score": number;
"num_episodes_watched": number;
"is_rewatching": boolean;
"start_date": string;
"finish_date": string;
"priority": number;
"num_times_rewatched": number;
"rewatch_value": number;
"tags": string[] | null;
"comments": string;
"updated_at": string;
}
export interface Media {
"id": number;
"idMal": number;
"title": MediaTitle;
"description": string;
"coverImage": {"ExtraLarge": string, "large": string, "Medium": string, "Color": string};
"BannerImage": string;
"Format": string;
"season": string;
"seasonYear": number;
"status": string;
"episodes": number;
"Duration": number;
"CountryOfOrigin": string;
"Source": string;
"Synonyms": string[] | null;
"AverageScore": number;
"MeanScore": number;
"Popularity": number;
"Trending": number;
"Favourites": number;
"relations": MediaRelations;
"startDate": MediaFuzzyDate;
"endDate": MediaFuzzyDate;
"nextAiringEpisode": {"airingAt": number, "timeUntilAiring": number, "episode": number};
"airingSchedule": MediaAiringSchedule;
"genres": string[] | null;
"tags": {"id": number, "name": string, "description": string, "rank": number, "isMediaSpoiler": boolean, "isAdult": boolean}[] | null;
"isAdult": boolean;
}
export interface MediaAiringSchedule {
"nodes": AiringScheduleNode[] | null;
}
export interface MediaFuzzyDate {
"year": number;
"month": number;
"day": number;
}
export interface MediaList {
"id": number;
"mediaId": number;
"userId": number;
"status": string;
"media": Media;
"startedAt": {"year": number, "month": number, "day": number};
"completedAt": {"year": number, "month": number, "day": number};
"notes": string;
"progress": number;
"score": number;
"repeat": number;
"user": {"id": number, "name": string, "avatar": {"large": string, "medium": string}, "statistics": {"anime": {"count": number, "statuses": {"status": string, "count": number}[] | null}}};
}
export interface MediaRelation {
"id": number;
"title": MediaTitle;
}
export interface MediaRelations {
"nodes": MediaRelation[] | null;
}
export interface MediaTitle {
"userPreferred": string;
"romaji": string;
"english": string;
"native": string;
}
export interface MyAnimeListUser {
"id": number;
"name": string;
"picture": string;
"gender": string;
"birthday": string;
"location": string;
"joined_at": string;
"anime_statistics": AnimeStatistics;
"time_zone": string;
"is_supporter": boolean;
}
export interface SimklAnime {
"last_watched_at": string;
"status": string;
"user_rating": number;
"last_watched": string;
"next_to_watch": string;
"watched_episodes_count": number;
"total_episodes_count": number;
"not_aired_episodes_count": number;
"show": {"title": string, "poster": string, "ids": {"simkl": number, "slug": string, "offjp": string, "tw": string, "ann": string, "mal": string, "wikien": string, "wikijp": string, "allcin": string, "imdb": string, "tmdb": string, "offen": string, "crunchyroll": string, "tvdbslug": string, "anilist": string, "animeplanet": string, "anisearch": string, "kitsu": string, "livechart": string, "traktslug": string, "anidb": string}};
"anime_type": string;
}
export interface SimklUser {
"user": {"name": string, "joined_at": string, "gender": string, "avatar": string, "bio": string, "loc": string, "age": string};
"account": {"id": number, "timezone": string, "type": string};
"connections": {"facebook": boolean};
}
export interface SimklWatchListType {
"anime": SimklAnime[] | null;
}
export interface StartedAt {
"year": number;
"month": number;
"day": number;
}
@@ -0,0 +1,9 @@
//@ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
Object.freeze($Create.Events);
@@ -0,0 +1,2 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
+7
View File
@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@wailsio/runtime": "3.0.0-beta.20",
"flowbite": "^2.5.1", "flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16", "flowbite-svelte": "^0.46.16",
"moment": "^2.30.1" "moment": "^2.30.1"
@@ -651,6 +652,12 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@wailsio/runtime": {
"version": "3.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-beta.20.tgz",
"integrity": "sha512-oPpWBgNK4QISP9/AweVnRIBg6hlEP7HVi0i4gVl0+6JEsQ6vDDTJXmbyy/j6AITPHWq7cWOYl2Ey5uucl1u3hw==",
"license": "MIT"
},
"node_modules/@yr/monotone-cubic-spline": { "node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
+2
View File
@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build:dev": "vite build --minify false --mode development",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json"
@@ -27,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@wailsio/runtime": "3.0.0-beta.20",
"flowbite": "^2.5.1", "flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16", "flowbite-svelte": "^0.46.16",
"moment": "^2.30.1" "moment": "^2.30.1"
+4 -8
View File
@@ -20,11 +20,7 @@
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte"; import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte"; import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import { import {App} from "../bindings/AniTrack";
CheckIfAniListLoggedIn,
GetMyAnimeList,
SimklGetUserWatchlist,
} from "../wailsjs/go/main/App";
import { loc } from "svelte-spa-router"; import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte"; import ErrorModal from "./helperComponents/ErrorModal.svelte";
@@ -49,10 +45,10 @@
await CheckIfAniListLoggedInAndLoadWatchList(); await CheckIfAniListLoggedInAndLoadWatchList();
} }
if ($malLoggedIn && $malPrimary) { if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w)); await App.GetMyAnimeList(1000).then((w) => malWatchList.set(w));
} }
if ($simklLoggedIn && $simklPrimary) { if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w)); await App.SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
} }
watchlistNeedsRefresh.set(false); watchlistNeedsRefresh.set(false);
@@ -67,7 +63,7 @@
"/": Home, "/": Home,
"/anime/:id": wrap({ "/anime/:id": wrap({
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"), asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
conditions: [async () => await CheckIfAniListLoggedIn()], conditions: [async () => await App.CheckIfAniListLoggedIn()],
loadingComponent: Spinner, loadingComponent: Spinner,
}), }),
// '*': "Not Found" // '*': "Not Found"
+9 -18
View File
@@ -28,16 +28,7 @@
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes"; import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes"; import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList"; import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import { import {App} from "../../bindings/AniTrack";
AniListDeleteEntry,
AniListUpdateEntry,
DeleteMyAnimeListEntry,
MyAnimeListUpdate,
SimklSyncEpisodes,
SimklSyncRating,
SimklSyncRemove,
SimklSyncStatus,
} from "../../wailsjs/go/main/App";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte"; import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte"; import Datepicker from "./Datepicker.svelte";
@@ -207,7 +198,7 @@
startedAt: convertDateToAniList(startedAtDate), startedAt: convertDateToAniList(startedAtDate),
completedAt: convertDateToAniList(completedAtDate), completedAt: convertDateToAniList(completedAtDate),
}; };
await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => { await App.AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => {
if (!value.data?.MediaList || value.data.MediaList.mediaId === 0) { if (!value.data?.MediaList || value.data.MediaList.mediaId === 0) {
setApiError( setApiError(
"anilist", "anilist",
@@ -261,7 +252,7 @@
comments: submitData.notes, comments: submitData.notes,
}; };
await MyAnimeListUpdate(currentMalAnime, body).then( await App.MyAnimeListUpdate(currentMalAnime, body).then(
(malAnimeReturn: MalListStatus) => { (malAnimeReturn: MalListStatus) => {
malAnime.update((value) => { malAnime.update((value) => {
value.my_list_status.status = malAnimeReturn.status; value.my_list_status.status = malAnimeReturn.status;
@@ -317,7 +308,7 @@
try { try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
if (currentSimklAnime.watched_episodes_count !== submitData.episodes) { if (currentSimklAnime.watched_episodes_count !== submitData.episodes) {
await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then( await App.SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
(value: SimklAnime) => { (value: SimklAnime) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
@@ -340,7 +331,7 @@
} }
if (currentSimklAnime.user_rating !== submitData.rating) { if (currentSimklAnime.user_rating !== submitData.rating) {
await SimklSyncRating(currentSimklAnime, submitData.rating).then( await App.SimklSyncRating(currentSimklAnime, submitData.rating).then(
(value) => { (value) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
@@ -363,7 +354,7 @@
} }
if (currentSimklAnime.status !== submitData.status.simkl) { if (currentSimklAnime.status !== submitData.status.simkl) {
await SimklSyncStatus( await App.SimklSyncStatus(
currentSimklAnime, currentSimklAnime,
submitData.status.simkl, submitData.status.simkl,
).then((value) => { ).then((value) => {
@@ -409,7 +400,7 @@
isAniListLoggedIn && isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
) { ) {
await AniListDeleteEntry(currentAniListAnime.data.MediaList.id); await App.AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `a-${currentAniListAnime.data.MediaList.mediaId}`, id: `a-${currentAniListAnime.data.MediaList.mediaId}`,
title, title,
@@ -436,7 +427,7 @@
try { try {
if (malLoggedIn && currentMalAnime.id !== 0) { if (malLoggedIn && currentMalAnime.id !== 0) {
await DeleteMyAnimeListEntry(currentMalAnime.id); await App.DeleteMyAnimeListEntry(currentMalAnime.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`, id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title, title: currentMalAnime.title,
@@ -463,7 +454,7 @@
try { try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
await SimklSyncRemove(currentSimklAnime); await App.SimklSyncRemove(currentSimklAnime);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${currentSimklAnime.show.ids.simkl}`, id: `s-${currentSimklAnime.show.ids.simkl}`,
title: currentSimklAnime.show.title, title: currentSimklAnime.show.title,
@@ -16,10 +16,10 @@
logoutOfSimkl, logoutOfSimkl,
serviceLoggingIn, serviceLoggingIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import * as runtime from "../../wailsjs/runtime"; import {Application} from "@wailsio/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes"; import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes"; import type { SimklUser } from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
let currentAniListUser: AniListUser; let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser; let currentMALUser: MyAnimeListUser;
@@ -189,14 +189,14 @@
<button <button
on:click={() => { on:click={() => {
dropdownUser(); dropdownUser();
ShowVersion(); App.ShowVersion();
}} }}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
> >
Version Version
</button> </button>
<button <button
on:click={() => runtime.Quit()} on:click={() => Application.Quit()}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
> >
Exit Application Exit Application
@@ -8,7 +8,7 @@
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType"; import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
let aniListWatchListLoaded: AniListCurrentUserWatchList; let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number; let page: number;
@@ -23,7 +23,7 @@
const perPageOptions = [10, 20, 50]; const perPageOptions = [10, 20, 50];
function ChangeWatchListPage(newPage: number) { function ChangeWatchListPage(newPage: number) {
GetAniListUserWatchingList(newPage, perPage, sort).then((result) => { App.GetAniListUserWatchingList(newPage, perPage, sort).then((result) => {
watchListPage.set(newPage); watchListPage.set(newPage);
aniListWatchlist.set(result); aniListWatchlist.set(result);
aniListLoggedIn.set(true); aniListLoggedIn.set(true);
@@ -43,7 +43,7 @@
function changeCountPerPage( function changeCountPerPage(
e: Event & { currentTarget: HTMLSelectElement }, e: Event & { currentTarget: HTMLSelectElement },
): void { ): void {
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then( App.GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
(result) => { (result) => {
animePerPage.set(Number(e.currentTarget.value)); animePerPage.set(Number(e.currentTarget.value));
watchListPage.set(1); watchListPage.set(1);
@@ -6,7 +6,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="button" type="button"
class="py-2 px-4 mt-4 mr-4 bg-gray-700 rounded-lg" class="py-2 px-4 mt-4 mr-4 text-nowrap bg-gray-700 rounded-lg"
on:click={async () => { on:click={async () => {
loading.set(true); loading.set(true);
await CheckIfAniListLoggedInAndLoadWatchList(); await CheckIfAniListLoggedInAndLoadWatchList();
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {AniListSearch} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import type {AniSearchList} from "../anilist/types/AniListTypes"; import type {AniSearchList} from "../anilist/types/AniListTypes";
import {push} from "svelte-spa-router"; import {push} from "svelte-spa-router";
@@ -81,7 +81,7 @@
reject(new Error("AniList is taking too long to respond. Please try again.")) reject(new Error("AniList is taking too long to respond. Please try again."))
}, SEARCH_TIMEOUT_MS) }, SEARCH_TIMEOUT_MS)
}) })
const result = await Promise.race([AniListSearch(term), timeout]) const result = await Promise.race([App.AniListSearch(term), timeout])
if (searchRequestId !== myRequest) { if (searchRequestId !== myRequest) {
return return
} }
+3 -3
View File
@@ -6,7 +6,7 @@
aniListWatchlist, aniListWatchlist,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { MediaListSort } from "../anilist/types/AniListTypes"; import { MediaListSort } from "../anilist/types/AniListTypes";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
const sortTypes = [ const sortTypes = [
{ value: MediaListSort.MediaId, name: "Media Id Asc" }, { value: MediaListSort.MediaId, name: "Media Id Asc" },
@@ -55,7 +55,7 @@
console.log(sort); console.log(sort);
async function changeWatchListSort() { async function changeWatchListSort() {
const result = await GetAniListUserWatchingList( const result = await App.GetAniListUserWatchingList(
$watchListPage, $watchListPage,
$animePerPage, $animePerPage,
$aniListSort, $aniListSort,
@@ -66,7 +66,7 @@
<select <select
id="sort" id="sort"
class="border rounded-lg block p-1.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500" class="border rounded-lg block max-h-10 mt-4 mx-2 p-1.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
bind:value={$aniListSort} bind:value={$aniListSort}
on:change={() => changeWatchListSort()} on:change={() => changeWatchListSort()}
> >
@@ -11,6 +11,7 @@
import loader from "../helperFunctions/loader"; import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Sort from "../helperComponents/Sort.svelte"; import Sort from "../helperComponents/Sort.svelte";
import RefreshWatchListButton from "./RefreshWatchListButton.svelte";
let isAniListLoggedIn: boolean; let isAniListLoggedIn: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList; let aniListWatchListLoaded: AniListCurrentUserWatchList;
@@ -26,7 +27,10 @@
> >
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h1 class="text-left text-xl font-bold">Your AniList WatchList</h1> <h1 class="text-left text-xl font-bold">Your AniList WatchList</h1>
<Sort /> <div class="flex">
<Sort />
<RefreshWatchListButton />
</div>
</div> </div>
<div <div
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { BrowserOpenURL } from "../../wailsjs/runtime"; import {Browser} from "@wailsio/runtime";
export let id: string; export let id: string;
export let url = ""; export let url = "";
@@ -25,7 +25,7 @@
<button <button
type="button" type="button"
class="underline underline-offset-2 px-4 py-1" class="underline underline-offset-2 px-4 py-1"
on:click={() => BrowserOpenURL(url)}>{newId}</button on:click={() => Browser.OpenURL(url)}>{newId}</button
> >
{:else} {:else}
{id} {id}
@@ -1,9 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {App} from "../../bindings/AniTrack";
CheckIfAniListLoggedIn,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
} from "../../wailsjs/go/main/App";
import { import {
aniListUser, aniListUser,
watchListPage, watchListPage,
@@ -29,7 +25,7 @@
export const LoadAniListUser = async () => { export const LoadAniListUser = async () => {
try { try {
await GetAniListLoggedInUser().then((user) => { await App.GetAniListLoggedInUser().then((user) => {
aniListUser.set(user); aniListUser.set(user);
}); });
} catch (err) { } catch (err) {
@@ -46,7 +42,7 @@
export const LoadAniListWatchList = async () => { export const LoadAniListWatchList = async () => {
try { try {
const watchList = await GetAniListUserWatchingList(page, perPage, sort); const watchList = await App.GetAniListUserWatchingList(page, perPage, sort);
aniListWatchlist.set(watchList); aniListWatchlist.set(watchList);
clearApiError(); clearApiError();
} catch (err) { } catch (err) {
@@ -63,7 +59,7 @@
export const CheckIfAniListLoggedInAndLoadWatchList = async () => { export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
serviceLoggingIn.update((s) => [...s, "anilist"]); serviceLoggingIn.update((s) => [...s, "anilist"]);
try { try {
const loggedIn = await CheckIfAniListLoggedIn(); const loggedIn = await App.CheckIfAniListLoggedIn();
if (loggedIn) { if (loggedIn) {
await LoadAniListUser(); await LoadAniListUser();
if (isAniListPrimary) await LoadAniListWatchList(); if (isAniListPrimary) await LoadAniListWatchList();
@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import {CheckIfMyAnimeListLoggedIn, GetMyAnimeList, GetMyAnimeListLoggedInUser} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte" import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte"
import type { MyAnimeListUser } from "../mal/types/MALTypes"; import type { MyAnimeListUser } from "../mal/types/MALTypes";
@@ -8,9 +8,9 @@
export const CheckIfMALLoggedInAndSetUser = async () => { export const CheckIfMALLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "mal"]) serviceLoggingIn.update((s) => [...s, "mal"])
await CheckIfMyAnimeListLoggedIn().then(loggedIn => { await App.CheckIfMyAnimeListLoggedIn().then(loggedIn => {
if (loggedIn) { if (loggedIn) {
GetMyAnimeListLoggedInUser().then(user => { App.GetMyAnimeListLoggedInUser().then(user => {
if (!user.name) { if (!user.name) {
malUser.set({} as MyAnimeListUser) malUser.set({} as MyAnimeListUser)
malLoggedIn.set(false) malLoggedIn.set(false)
@@ -18,7 +18,7 @@
} }
malUser.set(user) malUser.set(user)
if (isMalPrimary) { if (isMalPrimary) {
GetMyAnimeList(1000).then(watchList => { App.GetMyAnimeList(1000).then(watchList => {
malWatchList.set(watchList) malWatchList.set(watchList)
malLoggedIn.set(loggedIn) malLoggedIn.set(loggedIn)
}) })
@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import {CheckIfSimklLoggedIn, GetSimklLoggedInUser, SimklGetUserWatchlist} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import { simklLoggedIn, simklUser, simklPrimary, simklWatchList, serviceLoggingIn } from "./GlobalVariablesAndHelperFunctions.svelte"; import { simklLoggedIn, simklUser, simklPrimary, simklWatchList, serviceLoggingIn } from "./GlobalVariablesAndHelperFunctions.svelte";
let isSimklPrimary: boolean let isSimklPrimary: boolean
@@ -7,15 +7,15 @@
export const CheckIfSimklLoggedInAndSetUser = async () => { export const CheckIfSimklLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "simkl"]) serviceLoggingIn.update((s) => [...s, "simkl"])
await CheckIfSimklLoggedIn().then(loggedIn => { await App.CheckIfSimklLoggedIn().then(loggedIn => {
if (loggedIn) { if (loggedIn) {
GetSimklLoggedInUser().then(user => { App.GetSimklLoggedInUser().then(user => {
if (Object.keys(user).length === 0) { if (Object.keys(user).length === 0) {
simklLoggedIn.set(false) simklLoggedIn.set(false)
} else { } else {
simklUser.set(user) simklUser.set(user)
if (isSimklPrimary) { if (isSimklPrimary) {
SimklGetUserWatchlist().then(result => { App.SimklGetUserWatchlist().then(result => {
simklWatchList.set(result) simklWatchList.set(result)
simklLoggedIn.set(loggedIn) simklLoggedIn.set(loggedIn)
}) })
@@ -1,17 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {App} from "../../bindings/AniTrack";
GetAniListItem,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
GetMyAnimeListAnime,
GetMyAnimeListLoggedInUser,
GetSimklLoggedInUser,
LogoutAniList,
LogoutMyAnimeList,
LogoutSimkl,
SimklGetUserWatchlist,
SimklSearch,
} from "../../wailsjs/go/main/App";
import type { import type {
AniListCurrentUserWatchList, AniListCurrentUserWatchList,
AniListGetSingleAnime, AniListGetSingleAnime,
@@ -109,7 +97,7 @@
aniId: number, aniId: number,
login: boolean, login: boolean,
): Promise<""> { ): Promise<""> {
await GetAniListItem(aniId, login).then((aniListResult) => { await App.GetAniListItem(aniId, login).then((aniListResult) => {
let finalResult: AniListGetSingleAnime; let finalResult: AniListGetSingleAnime;
finalResult = aniListResult; finalResult = aniListResult;
if (login === false) { if (login === false) {
@@ -133,14 +121,14 @@
); );
}); });
if (isMalLoggedIn) { if (isMalLoggedIn) {
await GetMyAnimeListAnime( await App.GetMyAnimeListAnime(
currentAniListAnime.data.MediaList.media.idMal, currentAniListAnime.data.MediaList.media.idMal,
).then((malResult) => { ).then((malResult) => {
malAnime.set(malResult); malAnime.set(malResult);
}); });
} }
if (isSimklLoggedIn) { if (isSimklLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then( await App.SimklSearch(currentAniListAnime.data.MediaList).then(
(value: SimklAnime) => { (value: SimklAnime) => {
simklAnime.set(value); simklAnime.set(value);
}, },
@@ -168,13 +156,13 @@
export function loginToSimkl(): void { export function loginToSimkl(): void {
setServiceLoggingIn("simkl", true); setServiceLoggingIn("simkl", true);
GetSimklLoggedInUser() App.GetSimklLoggedInUser()
.then((user) => { .then((user) => {
if (Object.keys(user).length === 0) { if (Object.keys(user).length === 0) {
simklLoggedIn.set(false); simklLoggedIn.set(false);
} else { } else {
simklUser.set(user); simklUser.set(user);
SimklGetUserWatchlist().then((result) => { App.SimklGetUserWatchlist().then((result) => {
simklWatchList.set(result); simklWatchList.set(result);
simklLoggedIn.set(true); simklLoggedIn.set(true);
}); });
@@ -185,11 +173,11 @@
export function loginToAniList(): void { export function loginToAniList(): void {
setServiceLoggingIn("anilist", true); setServiceLoggingIn("anilist", true);
GetAniListLoggedInUser() App.GetAniListLoggedInUser()
.then((result) => { .then((result) => {
aniListUser.set(result); aniListUser.set(result);
if (isAniListPrimary) { if (isAniListPrimary) {
GetAniListUserWatchingList(page, perPage, sort).then((result) => { App.GetAniListUserWatchingList(page, perPage, sort).then((result) => {
aniListWatchlist.set(result); aniListWatchlist.set(result);
aniListLoggedIn.set(true); aniListLoggedIn.set(true);
}); });
@@ -202,7 +190,7 @@
export function loginToMAL(): void { export function loginToMAL(): void {
setServiceLoggingIn("mal", true); setServiceLoggingIn("mal", true);
GetMyAnimeListLoggedInUser() App.GetMyAnimeListLoggedInUser()
.then((result) => { .then((result) => {
if (!result.name) { if (!result.name) {
malUser.set({} as MyAnimeListUser); malUser.set({} as MyAnimeListUser);
@@ -216,7 +204,7 @@
} }
export function logoutOfAniList(): void { export function logoutOfAniList(): void {
LogoutAniList().then((result) => { App.LogoutAniList().then((result) => {
console.log(result); console.log(result);
if (Object.keys(aniWatchlist).length !== 0) { if (Object.keys(aniWatchlist).length !== 0) {
aniListWatchlist.set({} as AniListCurrentUserWatchList); aniListWatchlist.set({} as AniListCurrentUserWatchList);
@@ -227,7 +215,7 @@
} }
export function logoutOfMAL(): void { export function logoutOfMAL(): void {
LogoutMyAnimeList().then((result) => { App.LogoutMyAnimeList().then((result) => {
console.log(result); console.log(result);
malUser.set({} as MyAnimeListUser); malUser.set({} as MyAnimeListUser);
malLoggedIn.set(false); malLoggedIn.set(false);
@@ -235,7 +223,7 @@
} }
export function logoutOfSimkl(): void { export function logoutOfSimkl(): void {
LogoutSimkl().then((result) => { App.LogoutSimkl().then((result) => {
console.log(result); console.log(result);
simklUser.set({} as SimklUser); simklUser.set({} as SimklUser);
simklLoggedIn.set(false); simklLoggedIn.set(false);
-1
View File
@@ -46,7 +46,6 @@
</div> </div>
</div> </div>
{:else if isAniListLoggedIn && isAniListPrimary} {:else if isAniListLoggedIn && isAniListPrimary}
<RefreshWatchListButton />
<div class="container py-10"> <div class="container py-10">
<Pagination /> <Pagination />
<WatchList /> <WatchList />
+7 -1
View File
@@ -1,7 +1,13 @@
import {defineConfig} from 'vite' import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte' import {svelte} from '@sveltejs/vite-plugin-svelte'
import wails from '@wailsio/runtime/plugins/vite'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()] server: {
host: '127.0.0.1',
port: Number(process.env.WAILS_VITE_PORT) || 5173,
strictPort: true,
},
plugins: [svelte(), wails('./bindings')]
}) })
-61
View File
@@ -1,61 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function AniListBrowse(arg1:number,arg2:number,arg3:number,arg4:boolean,arg5:string,arg6:Array<string>,arg7:string,arg8:string,arg9:string,arg10:string,arg11:number,arg12:string,arg13:boolean,arg14:number,arg15:number,arg16:number,arg17:number,arg18:number,arg19:number,arg20:number,arg21:number,arg22:number,arg23:number,arg24:Array<number>,arg25:boolean,arg26:Array<string>,arg27:Array<string>,arg28:Array<string>,arg29:Array<string>,arg30:number,arg31:Array<string>):Promise<main.AniListCurrentUserWatchList>;
export function AniListDeleteEntry(arg1:number):Promise<main.DeleteAniListReturn>;
export function AniListLogin():Promise<void>;
export function AniListSearch(arg1:string):Promise<any>;
export function AniListUpdateEntry(arg1:main.AniListUpdateVariables):Promise<main.AniListGetSingleAnime>;
export function CheckIfAniListLoggedIn():Promise<boolean>;
export function CheckIfMyAnimeListLoggedIn():Promise<boolean>;
export function CheckIfSimklLoggedIn():Promise<boolean>;
export function DeleteMyAnimeListEntry(arg1:number):Promise<boolean>;
export function GetAniListItem(arg1:number,arg2:boolean):Promise<main.AniListGetSingleAnime>;
export function GetAniListLoggedInUser():Promise<main.AniListUser>;
export function GetAniListUserWatchingList(arg1:number,arg2:number,arg3:string):Promise<main.AniListCurrentUserWatchList>;
export function GetMyAnimeList(arg1:number):Promise<main.MALWatchlist>;
export function GetMyAnimeListAnime(arg1:number):Promise<main.MALAnime>;
export function GetMyAnimeListLoggedInUser():Promise<main.MyAnimeListUser>;
export function GetSimklLoggedInUser():Promise<main.SimklUser>;
export function LogoutAniList():Promise<string>;
export function LogoutMyAnimeList():Promise<string>;
export function LogoutSimkl():Promise<string>;
export function MyAnimeListLogin():Promise<void>;
export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>;
export function ShowVersion():Promise<void>;
export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>;
export function SimklLogin():Promise<void>;
export function SimklSearch(arg1:main.MediaList):Promise<main.SimklAnime>;
export function SimklSyncEpisodes(arg1:main.SimklAnime,arg2:number):Promise<main.SimklAnime>;
export function SimklSyncRating(arg1:main.SimklAnime,arg2:number):Promise<main.SimklAnime>;
export function SimklSyncRemove(arg1:main.SimklAnime):Promise<boolean>;
export function SimklSyncStatus(arg1:main.SimklAnime,arg2:string):Promise<main.SimklAnime>;
-119
View File
@@ -1,119 +0,0 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AniListBrowse(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25, arg26, arg27, arg28, arg29, arg30, arg31) {
return window['go']['main']['App']['AniListBrowse'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25, arg26, arg27, arg28, arg29, arg30, arg31);
}
export function AniListDeleteEntry(arg1) {
return window['go']['main']['App']['AniListDeleteEntry'](arg1);
}
export function AniListLogin() {
return window['go']['main']['App']['AniListLogin']();
}
export function AniListSearch(arg1) {
return window['go']['main']['App']['AniListSearch'](arg1);
}
export function AniListUpdateEntry(arg1) {
return window['go']['main']['App']['AniListUpdateEntry'](arg1);
}
export function CheckIfAniListLoggedIn() {
return window['go']['main']['App']['CheckIfAniListLoggedIn']();
}
export function CheckIfMyAnimeListLoggedIn() {
return window['go']['main']['App']['CheckIfMyAnimeListLoggedIn']();
}
export function CheckIfSimklLoggedIn() {
return window['go']['main']['App']['CheckIfSimklLoggedIn']();
}
export function DeleteMyAnimeListEntry(arg1) {
return window['go']['main']['App']['DeleteMyAnimeListEntry'](arg1);
}
export function GetAniListItem(arg1, arg2) {
return window['go']['main']['App']['GetAniListItem'](arg1, arg2);
}
export function GetAniListLoggedInUser() {
return window['go']['main']['App']['GetAniListLoggedInUser']();
}
export function GetAniListUserWatchingList(arg1, arg2, arg3) {
return window['go']['main']['App']['GetAniListUserWatchingList'](arg1, arg2, arg3);
}
export function GetMyAnimeList(arg1) {
return window['go']['main']['App']['GetMyAnimeList'](arg1);
}
export function GetMyAnimeListAnime(arg1) {
return window['go']['main']['App']['GetMyAnimeListAnime'](arg1);
}
export function GetMyAnimeListLoggedInUser() {
return window['go']['main']['App']['GetMyAnimeListLoggedInUser']();
}
export function GetSimklLoggedInUser() {
return window['go']['main']['App']['GetSimklLoggedInUser']();
}
export function LogoutAniList() {
return window['go']['main']['App']['LogoutAniList']();
}
export function LogoutMyAnimeList() {
return window['go']['main']['App']['LogoutMyAnimeList']();
}
export function LogoutSimkl() {
return window['go']['main']['App']['LogoutSimkl']();
}
export function MyAnimeListLogin() {
return window['go']['main']['App']['MyAnimeListLogin']();
}
export function MyAnimeListUpdate(arg1, arg2) {
return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2);
}
export function ShowVersion() {
return window['go']['main']['App']['ShowVersion']();
}
export function SimklGetUserWatchlist() {
return window['go']['main']['App']['SimklGetUserWatchlist']();
}
export function SimklLogin() {
return window['go']['main']['App']['SimklLogin']();
}
export function SimklSearch(arg1) {
return window['go']['main']['App']['SimklSearch'](arg1);
}
export function SimklSyncEpisodes(arg1, arg2) {
return window['go']['main']['App']['SimklSyncEpisodes'](arg1, arg2);
}
export function SimklSyncRating(arg1, arg2) {
return window['go']['main']['App']['SimklSyncRating'](arg1, arg2);
}
export function SimklSyncRemove(arg1) {
return window['go']['main']['App']['SimklSyncRemove'](arg1);
}
export function SimklSyncStatus(arg1, arg2) {
return window['go']['main']['App']['SimklSyncStatus'](arg1, arg2);
}
-974
View File
@@ -1,974 +0,0 @@
export namespace main {
export class AiringScheduleNode {
id: number;
airingAt: number;
timeUntilAiring: number;
episode: number;
mediaId: number;
static createFrom(source: any = {}) {
return new AiringScheduleNode(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.airingAt = source["airingAt"];
this.timeUntilAiring = source["timeUntilAiring"];
this.episode = source["episode"];
this.mediaId = source["mediaId"];
}
}
export class AniListCurrentUserWatchList {
data: struct { Page struct { PageInfo struct { Total int "json:\"total\""; PerPage int "json:\"perPage\""; CurrentPage int "json:\"currentPage\""; LastPage int "json:\"lastPage\""; HasNextPage bool "json:\"hasNextPage\"" } "json:\"pageInfo\""; MediaList []main.;
static createFrom(source: any = {}) {
return new AniListCurrentUserWatchList(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AniListGetSingleAnime {
data: struct { MediaList main.;
static createFrom(source: any = {}) {
return new AniListGetSingleAnime(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class CompletedAt {
year: number;
month: number;
day: number;
static createFrom(source: any = {}) {
return new CompletedAt(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.year = source["year"];
this.month = source["month"];
this.day = source["day"];
}
}
export class StartedAt {
year: number;
month: number;
day: number;
static createFrom(source: any = {}) {
return new StartedAt(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.year = source["year"];
this.month = source["month"];
this.day = source["day"];
}
}
export class AniListUpdateVariables {
mediaId: number;
progress: number;
status: string;
score: number;
repeat: number;
notes: string;
startedAt: StartedAt;
completedAt: CompletedAt;
static createFrom(source: any = {}) {
return new AniListUpdateVariables(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.mediaId = source["mediaId"];
this.progress = source["progress"];
this.status = source["status"];
this.score = source["score"];
this.repeat = source["repeat"];
this.notes = source["notes"];
this.startedAt = this.convertValues(source["startedAt"], StartedAt);
this.completedAt = this.convertValues(source["completedAt"], CompletedAt);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AniListUser {
// Go type: struct { Viewer struct { ID int "json:\"id\""; Name string "json:\"name\""; Avatar struct { Large string "json:\"large\""; Medium string "json:\"medium\"" } "json:\"avatar\""; BannerImage string "json:\"bannerImage\""; SiteUrl string "json:\"siteUrl\"" } "json:\"Viewer\"" }
data: any;
static createFrom(source: any = {}) {
return new AniListUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class DeleteAniListReturn {
// Go type: struct { DeleteMediaListEntry struct { Deleted bool "json:\"deleted\"" } "json:\"DeleteMediaListEntry\"" }
data: any;
static createFrom(source: any = {}) {
return new DeleteAniListReturn(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MALAnime {
id: id;
title: title;
main_picture: mainPicture;
alternative_titles: alternativeTitles;
start_date: startDate;
end_date: endDate;
synopsis: synopsis;
mean: mean;
rank: rank;
popularity: popularity;
num_list_users: numListUsers;
num_scoring_users: numScoringUsers;
nsfw: nsfw;
genres: genres;
created_at: createdAt;
updated_at: updatedAt;
media_type: mediaType;
status: status;
my_list_status: MalListStatus;
num_episodes: numEpisodes;
start_season: startSeason;
broadcast: broadcast;
source: source;
average_episode_duration: averageEpisodeDuration;
rating: rating;
studios: studios;
pictures: pictures;
background: background;
related_anime: relatedAnime;
recommendations: recommendations;
Statistics: struct { NumListUsers int "json:\"num_list_users\" ts_type:\"numListUsers\""; Status struct { Watching main.;
static createFrom(source: any = {}) {
return new MALAnime(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.title = source["title"];
this.main_picture = source["main_picture"];
this.alternative_titles = source["alternative_titles"];
this.start_date = source["start_date"];
this.end_date = source["end_date"];
this.synopsis = source["synopsis"];
this.mean = source["mean"];
this.rank = source["rank"];
this.popularity = source["popularity"];
this.num_list_users = source["num_list_users"];
this.num_scoring_users = source["num_scoring_users"];
this.nsfw = source["nsfw"];
this.genres = source["genres"];
this.created_at = source["created_at"];
this.updated_at = source["updated_at"];
this.media_type = source["media_type"];
this.status = source["status"];
this.my_list_status = source["my_list_status"];
this.num_episodes = source["num_episodes"];
this.start_season = source["start_season"];
this.broadcast = source["broadcast"];
this.source = source["source"];
this.average_episode_duration = source["average_episode_duration"];
this.rating = source["rating"];
this.studios = source["studios"];
this.pictures = source["pictures"];
this.background = source["background"];
this.related_anime = source["related_anime"];
this.recommendations = source["recommendations"];
this.Statistics = this.convertValues(source["Statistics"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MALUploadStatus {
status: string;
is_rewatching: boolean;
score: number;
num_watched_episodes: number;
num_times_rewatched: number;
comments: string;
static createFrom(source: any = {}) {
return new MALUploadStatus(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.is_rewatching = source["is_rewatching"];
this.score = source["score"];
this.num_watched_episodes = source["num_watched_episodes"];
this.num_times_rewatched = source["num_times_rewatched"];
this.comments = source["comments"];
}
}
export class MALWatchlist {
data: data;
paging: paging;
static createFrom(source: any = {}) {
return new MALWatchlist(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = source["data"];
this.paging = source["paging"];
}
}
export class MalListStatus {
status: status;
score: score;
num_episodes_watched: numEpisodesWatched;
is_rewatching: isRewatching;
start_date: startDate;
finish_date: finishDate;
priority: priority;
num_times_rewatched: numTimesRewatched;
rewatch_value: rewatchValue;
tags: tags;
comments: comments;
updated_at: updatedAt;
static createFrom(source: any = {}) {
return new MalListStatus(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.score = source["score"];
this.num_episodes_watched = source["num_episodes_watched"];
this.is_rewatching = source["is_rewatching"];
this.start_date = source["start_date"];
this.finish_date = source["finish_date"];
this.priority = source["priority"];
this.num_times_rewatched = source["num_times_rewatched"];
this.rewatch_value = source["rewatch_value"];
this.tags = source["tags"];
this.comments = source["comments"];
this.updated_at = source["updated_at"];
}
}
export class {
id: number;
name: string;
description: string;
rank: number;
isMediaSpoiler: boolean;
isAdult: boolean;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.name = source["name"];
this.description = source["description"];
this.rank = source["rank"];
this.isMediaSpoiler = source["isMediaSpoiler"];
this.isAdult = source["isAdult"];
}
}
export class MediaAiringSchedule {
nodes: AiringScheduleNode[];
static createFrom(source: any = {}) {
return new MediaAiringSchedule(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.nodes = this.convertValues(source["nodes"], AiringScheduleNode);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MediaFuzzyDate {
year: number;
month: number;
day: number;
static createFrom(source: any = {}) {
return new MediaFuzzyDate(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.year = source["year"];
this.month = source["month"];
this.day = source["day"];
}
}
export class MediaRelation {
id: number;
title: MediaTitle;
static createFrom(source: any = {}) {
return new MediaRelation(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.title = this.convertValues(source["title"], MediaTitle);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MediaRelations {
nodes: MediaRelation[];
static createFrom(source: any = {}) {
return new MediaRelations(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.nodes = this.convertValues(source["nodes"], MediaRelation);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MediaTitle {
userPreferred: string;
romaji: string;
english: string;
native: string;
static createFrom(source: any = {}) {
return new MediaTitle(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.userPreferred = source["userPreferred"];
this.romaji = source["romaji"];
this.english = source["english"];
this.native = source["native"];
}
}
export class Media {
id: number;
idMal: number;
title: MediaTitle;
description: string;
// Go type: struct { ExtraLarge string; Large string "json:\"large\""; Medium string; Color string }
coverImage: any;
BannerImage: string;
Format: string;
season: string;
seasonYear: number;
status: string;
episodes: number;
Duration: number;
CountryOfOrigin: string;
Source: string;
Synonyms: string[];
AverageScore: number;
MeanScore: number;
Popularity: number;
Trending: number;
Favourites: number;
relations: MediaRelations;
startDate: MediaFuzzyDate;
endDate: MediaFuzzyDate;
// Go type: struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" }
nextAiringEpisode: any;
airingSchedule: MediaAiringSchedule;
genres: string[];
tags: [];
isAdult: boolean;
static createFrom(source: any = {}) {
return new Media(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.idMal = source["idMal"];
this.title = this.convertValues(source["title"], MediaTitle);
this.description = source["description"];
this.coverImage = this.convertValues(source["coverImage"], Object);
this.BannerImage = source["BannerImage"];
this.Format = source["Format"];
this.season = source["season"];
this.seasonYear = source["seasonYear"];
this.status = source["status"];
this.episodes = source["episodes"];
this.Duration = source["Duration"];
this.CountryOfOrigin = source["CountryOfOrigin"];
this.Source = source["Source"];
this.Synonyms = source["Synonyms"];
this.AverageScore = source["AverageScore"];
this.MeanScore = source["MeanScore"];
this.Popularity = source["Popularity"];
this.Trending = source["Trending"];
this.Favourites = source["Favourites"];
this.relations = this.convertValues(source["relations"], MediaRelations);
this.startDate = this.convertValues(source["startDate"], MediaFuzzyDate);
this.endDate = this.convertValues(source["endDate"], MediaFuzzyDate);
this.nextAiringEpisode = this.convertValues(source["nextAiringEpisode"], Object);
this.airingSchedule = this.convertValues(source["airingSchedule"], MediaAiringSchedule);
this.genres = source["genres"];
this.tags = this.convertValues(source["tags"], );
this.isAdult = source["isAdult"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MediaList {
id: number;
mediaId: number;
userId: number;
status: string;
media: Media;
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
startedAt: any;
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
completedAt: any;
notes: string;
progress: number;
score: number;
repeat: number;
// Go type: struct { ID int "json:\"id\""; Name string "json:\"name\""; Avatar struct { Large string "json:\"large\""; Medium string "json:\"medium\"" } "json:\"avatar\""; Statistics struct { Anime struct { Count int "json:\"count\""; Statuses []struct { Status string "json:\"status\""; Count int "json:\"count\"" } "json:\"statuses\"" } "json:\"anime\"" } "json:\"statistics\"" }
user: any;
static createFrom(source: any = {}) {
return new MediaList(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.mediaId = source["mediaId"];
this.userId = source["userId"];
this.status = source["status"];
this.media = this.convertValues(source["media"], Media);
this.startedAt = this.convertValues(source["startedAt"], Object);
this.completedAt = this.convertValues(source["completedAt"], Object);
this.notes = source["notes"];
this.progress = source["progress"];
this.score = source["score"];
this.repeat = source["repeat"];
this.user = this.convertValues(source["user"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MyAnimeListUser {
id: id;
name: name;
picture: picture;
gender: gender;
birthday: birthday;
location: location;
joined_at: joinedAt;
num_items_watching: numItemsWatching;
num_items_completed: numItemsCompleted;
num_items_on_hold: numItemsOnHold;
num_items_dropped: numItemsDropped;
num_items_plan_to_watch: numItemsPlanToWatch;
num_items: numItems;
num_days_watched: numDaysWatched;
num_days_watching: numDaysWatching;
num_days_completed: numDaysCompleted;
num_days_on_hold: numDaysOnHold;
num_days_dropped: numDaysDropped;
num_days: numDays;
num_episodes: numEpisodes;
num_times_rewatched: numTimesRewatched;
mean_score: meanScore;
time_zone: timeZone;
is_supporter: isSupporter;
static createFrom(source: any = {}) {
return new MyAnimeListUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.name = source["name"];
this.picture = source["picture"];
this.gender = source["gender"];
this.birthday = source["birthday"];
this.location = source["location"];
this.joined_at = source["joined_at"];
this.num_items_watching = source["num_items_watching"];
this.num_items_completed = source["num_items_completed"];
this.num_items_on_hold = source["num_items_on_hold"];
this.num_items_dropped = source["num_items_dropped"];
this.num_items_plan_to_watch = source["num_items_plan_to_watch"];
this.num_items = source["num_items"];
this.num_days_watched = source["num_days_watched"];
this.num_days_watching = source["num_days_watching"];
this.num_days_completed = source["num_days_completed"];
this.num_days_on_hold = source["num_days_on_hold"];
this.num_days_dropped = source["num_days_dropped"];
this.num_days = source["num_days"];
this.num_episodes = source["num_episodes"];
this.num_times_rewatched = source["num_times_rewatched"];
this.mean_score = source["mean_score"];
this.time_zone = source["time_zone"];
this.is_supporter = source["is_supporter"];
}
}
export class SimklAnime {
last_watched_at: last_watched_at;
status: status;
user_rating: user_rating;
last_watched: last_watched;
next_to_watch: next_to_watch;
watched_episodes_count: watched_episodes_count;
total_episodes_count: total_episodes_count;
not_aired_episodes_count: not_aired_episodes_count;
show: show;
anime_type: anime_type;
static createFrom(source: any = {}) {
return new SimklAnime(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.last_watched_at = source["last_watched_at"];
this.status = source["status"];
this.user_rating = source["user_rating"];
this.last_watched = source["last_watched"];
this.next_to_watch = source["next_to_watch"];
this.watched_episodes_count = source["watched_episodes_count"];
this.total_episodes_count = source["total_episodes_count"];
this.not_aired_episodes_count = source["not_aired_episodes_count"];
this.show = source["show"];
this.anime_type = source["anime_type"];
}
}
export class SimklUser {
user: user;
account: account;
connections: connections;
static createFrom(source: any = {}) {
return new SimklUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.user = source["user"];
this.account = source["account"];
this.connections = source["connections"];
}
}
export class SimklWatchListType {
anime: anime;
static createFrom(source: any = {}) {
return new SimklWatchListType(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.anime = source["anime"];
}
}
}
export namespace struct { MediaList main {
export class {
MediaList: main.MediaList;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.MediaList = this.convertValues(source["MediaList"], main.MediaList);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace struct { Node main {
export class {
node: node;
num_recommendations: numRecommendations;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.num_recommendations = source["num_recommendations"];
}
}
export class {
node: node;
relation_type: relationType;
relation_type_formatted: relationTypeFormatted;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.relation_type = source["relation_type"];
this.relation_type_formatted = source["relation_type_formatted"];
}
}
}
export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" ts_type:\"medium\""; Large string "json:\"large\" ts_type:\"large\"" } "json:\"main_picture\" ts_type:\"mainPicture\"" } "json:\"node\" ts_type:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
export class {
node: node;
list_status: listStatus;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.list_status = source["list_status"];
}
}
}
export namespace struct { NumListUsers int "json:\"num_list_users\" ts_type:\"numListUsers\""; Status struct { Watching main {
export class {
num_list_users: numListUsers;
Status: struct { Watching main.;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.num_list_users = source["num_list_users"];
this.Status = this.convertValues(source["Status"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
export class {
status: status;
score: score;
num_episodes_watched: numEpisodesWatched;
is_rewatching: isRewatching;
updated_at: updatedAt;
start_date: startDate;
finish_date: finishDate;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.score = source["score"];
this.num_episodes_watched = source["num_episodes_watched"];
this.is_rewatching = source["is_rewatching"];
this.updated_at = source["updated_at"];
this.start_date = source["start_date"];
this.finish_date = source["finish_date"];
}
}
}
export namespace struct { Watching main {
export class {
watching: string;
completed: string;
on_hold: string;
dropped: string;
plan_to_watch: string;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.watching = source["watching"];
this.completed = source["completed"];
this.on_hold = source["on_hold"];
this.dropped = source["dropped"];
this.plan_to_watch = source["plan_to_watch"];
}
}
}
-24
View File
@@ -1,24 +0,0 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}
-330
View File
@@ -1,330 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void
// Notification types
export interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
export interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
export interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
// Initializes the notification service for the application.
// This must be called before sending any notifications.
export function InitializeNotifications(): Promise<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
// Sends a notification with action buttons. Requires a registered category.
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
// Registers a notification category that can be used with SendNotificationWithActions.
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
// Removes a notification by its identifier (cross-platform convenience function).
export function RemoveNotification(identifier: string): Promise<void>;
-298
View File
@@ -1,298 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}
export function InitializeNotifications() {
return window.runtime.InitializeNotifications();
}
export function CleanupNotifications() {
return window.runtime.CleanupNotifications();
}
export function IsNotificationAvailable() {
return window.runtime.IsNotificationAvailable();
}
export function RequestNotificationAuthorization() {
return window.runtime.RequestNotificationAuthorization();
}
export function CheckNotificationAuthorization() {
return window.runtime.CheckNotificationAuthorization();
}
export function SendNotification(options) {
return window.runtime.SendNotification(options);
}
export function SendNotificationWithActions(options) {
return window.runtime.SendNotificationWithActions(options);
}
export function RegisterNotificationCategory(category) {
return window.runtime.RegisterNotificationCategory(category);
}
export function RemoveNotificationCategory(categoryId) {
return window.runtime.RemoveNotificationCategory(categoryId);
}
export function RemoveAllPendingNotifications() {
return window.runtime.RemoveAllPendingNotifications();
}
export function RemovePendingNotification(identifier) {
return window.runtime.RemovePendingNotification(identifier);
}
export function RemoveAllDeliveredNotifications() {
return window.runtime.RemoveAllDeliveredNotifications();
}
export function RemoveDeliveredNotification(identifier) {
return window.runtime.RemoveDeliveredNotification(identifier);
}
export function RemoveNotification(identifier) {
return window.runtime.RemoveNotification(identifier);
}
+4 -33
View File
@@ -3,49 +3,20 @@ module AniTrack
go 1.25.0 go 1.25.0
require ( require (
github.com/99designs/keyring v1.2.2
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.19.0
github.com/wailsapp/wails/v2 v2.15.0 github.com/wailsapp/wails/v3 v3.0.0-beta.20
github.com/zalando/go-keyring v0.2.8
) )
require ( require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/labstack/echo/v4 v4.15.2 // indirect
github.com/labstack/gommon v0.5.0 // 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.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mtibben/percent v0.2.1 // 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.53.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.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.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.39.0 // indirect
) )
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod
+12 -80
View File
@@ -1,70 +1,27 @@
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/dvsekhvalnov/jose2go v1.8.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 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/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
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=
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
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 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -75,37 +32,12 @@ github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/wailsapp/wails/v3 v3.0.0-beta.20 h1:AKKrRGMSqzJET1W0Jt9v+f0oOqM9Amsi7LKMM6XnSOE=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/wailsapp/wails/v3 v3.0.0-beta.20/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
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.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.23/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.15.0 h1:u7cHK+UesZOlYxyJxfYLteaCPhws6UsZoDdqUejuX6Q=
github.com/wailsapp/wails/v2 v2.15.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+46 -27
View File
@@ -2,45 +2,64 @@ package main
import ( import (
"embed" "embed"
"log"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
) )
//go:embed all:frontend/dist //go:embed all:frontend/dist
var assets embed.FS var assets embed.FS
func main() { func main() {
// Create an instance of the app structure // The service is created after the application (it needs the app
app := NewApp() // reference), so the single-instance callback closes over the variable
// and resolves it at call time. A second instance can only launch once
// this one is running, by which point svc is always set.
var svc *App
// Create application with options // Create application with options
err := wails.Run(&options.App{ app := application.New(application.Options{
Title: "AniTrack", Name: "AniTrack",
Width: 1024, Description: "Track anime watchlists across AniList, MyAnimeList, and Simkl",
Height: 768, Assets: application.AssetOptions{
AssetServer: &assetserver.Options{ Handler: application.AssetFileServerFS(assets),
Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, SingleInstance: &application.SingleInstanceOptions{
OnStartup: app.startup, UniqueID: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b",
SingleInstanceLock: &options.SingleInstanceLock{ OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
UniqueId: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b", svc.onSecondInstanceLaunch(data)
OnSecondInstanceLaunch: app.onSecondInstanceLaunch, },
}, },
Bind: []interface{}{ Linux: application.LinuxOptions{
app, ProgramName: "AniTrack",
},
Linux: &linux.Options{
Icon: []byte("./build/AniTrack.png"),
WindowIsTranslucent: false,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
ProgramName: "AniTrack",
}, },
}) })
if err != nil {
println("Error:", err.Error()) svc = NewApp(app)
app.RegisterService(application.NewService(svc))
// Self-updates: desktop production builds only (no-op elsewhere).
maybeEnableUpdater(app)
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: appTitle(),
Width: 1024,
Height: 768,
BackgroundColour: application.NewRGBA(27, 38, 54, 1),
URL: "/",
Linux: application.LinuxWindow{
Icon: []byte("./build/AniTrack.png"),
WindowIsTranslucent: false,
// OnDemand: GPU for scrolling/compositing on AMD/Intel Mesa,
// software fallback otherwise. Never (the old Nvidia/X11
// blank-window workaround) forces CPU raster on GTK4/WebKit 6
// and makes scrolling janky. All target machines here are
// AMD or Intel, so the Nvidia DMABUF workaround is not needed.
WebviewGpuPolicy: application.WebviewGpuPolicyOnDemand,
},
})
if err := app.Run(); err != nil {
log.Fatal(err)
} }
} }
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"context"
_ "embed"
"log"
"os"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/updater"
giteaprovider "AniTrack/updater/gitea"
)
//go:embed updater.pub
var updaterPublicKey []byte
// maybeEnableUpdater wires self-updates: desktop production builds only.
// Mobile stays on Obtainium; dev builds never phone home. The startup check
// is headless — the builtin window opens only when an update is found, via a
// second CheckAndInstall (one redundant index round-trip, negligible).
func maybeEnableUpdater(app *application.App) {
if !updaterAutoCheck {
return
}
if !application.System.IsDesktop() {
return
}
gh, err := giteaprovider.New(giteaprovider.Config{
BaseURL: "https://git.linuxhg.com",
Owner: "john-okeefe",
Repo: "Anitrack",
AssetName: "AniTrack-linux-amd64",
// Beta channel (rc tags) for testing: ANITRACK_UPDATER_CHANNEL=beta.
AllowPrerelease: os.Getenv("ANITRACK_UPDATER_CHANNEL") == "beta",
})
if err != nil {
log.Printf("updater: %s", err)
return
}
if err := app.Updater.Init(updater.Config{
CurrentVersion: appVersion(),
Providers: []updater.Provider{gh},
PublicKey: updaterPublicKey,
Window: &updater.BuiltinWindow{
CSS: ":root { --accent: #4d9fff; }",
},
}); err != nil {
log.Printf("updater: init: %s", err)
return
}
go func() {
rel, err := app.Updater.Check(context.Background())
if err != nil {
log.Printf("updater: check: %s", err)
return
}
if rel == nil {
return
}
log.Printf("updater: %s available, opening installer", rel.Version)
if err := app.Updater.CheckAndInstall(context.Background()); err != nil {
log.Printf("updater: install: %s", err)
}
}()
}
+3
View File
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAWWF3cDxJKM668FqWsFsA9e7r7aXU2uDTUF+ZKKcxLP0=
-----END PUBLIC KEY-----
+329
View File
@@ -0,0 +1,329 @@
// Package gitea implements updater.Provider for Gitea releases.
//
// Releases live on a self-hosted Gitea instance, for which Wails ships no
// in-tree provider, so this mirrors the in-tree github provider against
// Gitea's releases API. Everything else (verification, staging, swap,
// restart, window) stays the framework's job.
//
// Asset layout per release (see .gitea/workflows/release.yml):
//
// AniTrack-linux-amd64 bare updater binary, exact name match
// AniTrack-linux-amd64.sha512 "<hex> <filename>" (sha512sum format)
// AniTrack-linux-amd64.sig base64 ed25519ph signature from
// `wails3 updater sign`
//
// A release missing any of the three is skipped: verification fails closed.
package gitea
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/wailsapp/wails/v3/pkg/updater"
)
// Config configures the Gitea provider.
type Config struct {
// BaseURL is the instance root, e.g. https://git.linuxhg.com.
BaseURL string
// Owner and Repo identify the repository, e.g. john-okeefe/Anitrack.
Owner string
Repo string
// AssetName is the exact updater artifact name, e.g. AniTrack-linux-amd64.
AssetName string
// HTTPClient overrides the default client (30s timeout). Optional.
HTTPClient *http.Client
// AllowPrerelease includes pre-release-tagged releases. Default false:
// -rc tags stay invisible (stable channel). Beta/test flows opt in.
AllowPrerelease bool
}
// Provider is an updater.Provider backed by Gitea releases.
type Provider struct {
cfg Config
client *http.Client
}
// New validates config and returns a Provider.
func New(cfg Config) (*Provider, error) {
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
if cfg.BaseURL == "" || cfg.Owner == "" || cfg.Repo == "" || cfg.AssetName == "" {
return nil, fmt.Errorf("gitea: BaseURL, Owner, Repo and AssetName are all required")
}
client := cfg.HTTPClient
if client == nil {
client = &http.Client{Timeout: 30 * time.Second}
}
return &Provider{cfg: cfg, client: client}, nil
}
// Name identifies the provider in logs and event payloads.
func (p *Provider) Name() string { return "gitea" }
// giteaRelease mirrors the fields we read from the Gitea releases API.
type giteaRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
PublishedAt string `json:"published_at"`
Assets []giteaAsset `json:"assets"`
}
// giteaAsset mirrors one release attachment.
type giteaAsset struct {
Name string `json:"name"`
Size int64 `json:"size"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// Check finds the newest usable release newer than req.CurrentVersion.
// (nil, nil) means up-to-date.
func (p *Provider) Check(ctx context.Context, req updater.CheckRequest) (*updater.Release, error) {
releases, err := p.listReleases(ctx)
if err != nil {
return nil, err
}
var best *giteaRelease
var bestAsset *giteaAsset
for i := range releases {
r := &releases[i]
if r.Draft {
continue
}
if r.Prerelease && !p.cfg.AllowPrerelease {
continue
}
if compareSemver(r.TagName, req.CurrentVersion) <= 0 {
continue
}
asset := findAsset(r.Assets, p.cfg.AssetName)
if asset == nil {
continue
}
if best == nil || compareSemver(r.TagName, best.TagName) > 0 {
best = r
bestAsset = asset
}
}
if best == nil {
return nil, nil
}
verification, err := p.fetchVerification(ctx, *bestAsset)
if err != nil {
return nil, err
}
rel := &updater.Release{
Version: strings.TrimPrefix(strings.TrimPrefix(best.TagName, "v"), "V"),
Channel: "stable",
Name: best.Name,
Notes: best.Body,
Artifact: updater.Artifact{
Filename: bestAsset.Name,
Size: bestAsset.Size,
Platform: req.Platform,
Arch: req.Arch,
},
Verification: verification,
Metadata: map[string]any{
"downloadURL": bestAsset.BrowserDownloadURL,
},
}
if rel.Channel == "stable" && (best.Prerelease) {
rel.Channel = "beta"
}
if t, err := time.Parse(time.RFC3339, best.PublishedAt); err == nil {
rel.PublishedAt = t
}
return rel, nil
}
// Download streams the release artifact to dst, reporting progress.
func (p *Provider) Download(ctx context.Context, r *updater.Release, dst io.Writer, onProgress func(written, total int64)) error {
url, _ := r.Metadata["downloadURL"].(string)
if url == "" {
return fmt.Errorf("gitea: release %s has no download URL", r.Version)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "AniTrack-updater")
resp, err := p.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("gitea: download %s: %s", url, resp.Status)
}
total := r.Artifact.Size
if total <= 0 {
total = resp.ContentLength
}
var written int64
buf := make([]byte, 128*1024)
for {
n, rerr := resp.Body.Read(buf)
if n > 0 {
if _, werr := dst.Write(buf[:n]); werr != nil {
return werr
}
written += int64(n)
onProgress(written, total)
}
if rerr == io.EOF {
onProgress(written, total)
return nil
}
if rerr != nil {
return rerr
}
}
}
// listReleases fetches the releases index (newest first page is enough).
func (p *Provider) listReleases(ctx context.Context) ([]giteaRelease, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases?limit=50", p.cfg.BaseURL, p.cfg.Owner, p.cfg.Repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "AniTrack-updater")
resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("gitea: list releases: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gitea: list releases: %s", resp.Status)
}
var releases []giteaRelease
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return nil, fmt.Errorf("gitea: decode releases: %w", err)
}
return releases, nil
}
// findAsset returns the attachment with exactly assetName, or nil.
func findAsset(assets []giteaAsset, assetName string) *giteaAsset {
for i := range assets {
if assets[i].Name == assetName && assets[i].BrowserDownloadURL != "" {
return &assets[i]
}
}
return nil
}
// fetchVerification downloads the .sha512 + .sig sidecars next to the asset.
// This mirrors `wails3 updater sign` (SHA-512 digest, ed25519ph signature).
// Anything missing or malformed is an error: verification fails closed.
func (p *Provider) fetchVerification(ctx context.Context, asset giteaAsset) (*updater.Verification, error) {
base := strings.TrimSuffix(asset.BrowserDownloadURL, asset.Name)
digestHex, err := p.fetchText(ctx, base+asset.Name+".sha512")
if err != nil {
return nil, fmt.Errorf("gitea: digest sidecar: %w", err)
}
sigB64, err := p.fetchText(ctx, base+asset.Name+".sig")
if err != nil {
return nil, fmt.Errorf("gitea: signature sidecar: %w", err)
}
// "<hex> <filename>" (sha512sum format); tolerate bare hex too.
digestHex = strings.Fields(digestHex)[0]
digest, err := hex.DecodeString(digestHex)
if err != nil || len(digest) != 64 {
return nil, fmt.Errorf("gitea: malformed digest sidecar for %s", asset.Name)
}
sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sigB64))
if err != nil || len(sig) == 0 {
return nil, fmt.Errorf("gitea: malformed signature sidecar for %s", asset.Name)
}
return &updater.Verification{
DigestAlgo: "sha512",
Digest: digest,
SignatureAlgo: "ed25519ph",
Signature: sig,
}, nil
}
// fetchText GETs url and returns the trimmed body.
func (p *Provider) fetchText(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "AniTrack-updater")
resp, err := p.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%s: %s", url, resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 8*1024))
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// compareSemver compares plain versions ("1.99.0", optional v prefix).
// Pre-release suffixes (-rc1) sort below the plain release. Returns
// -1, 0 or +1. Unparseable input compares as 0.0.0 (never newest).
func compareSemver(a, b string) int {
an, ap := splitSemver(a)
bn, bp := splitSemver(b)
for i := 0; i < 3; i++ {
if an[i] != bn[i] {
if an[i] < bn[i] {
return -1
}
return 1
}
}
switch {
case ap == bp:
return 0
case ap == "":
return 1
case bp == "":
return -1
case ap < bp:
return -1
case ap > bp:
return 1
}
return 0
}
// splitSemver splits "v1.99.0-rc1" into ([1 99 0], "rc1").
func splitSemver(v string) ([3]int, string) {
var nums [3]int
v = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(v), "v"), "V")
core, pre, _ := strings.Cut(v, "-")
for i, part := range strings.Split(core, ".") {
if i >= 3 {
break
}
n, err := strconv.Atoi(part)
if err != nil {
return [3]int{}, pre
}
nums[i] = n
}
return nums, pre
}
+291
View File
@@ -0,0 +1,291 @@
package gitea
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/wailsapp/wails/v3/pkg/updater"
)
func TestCompareSemver(t *testing.T) {
cases := []struct {
a, b string
want int
}{
{"1.99.0", "1.6.8", 1},
{"1.6.8", "1.99.0", -1},
{"1.99.0", "1.99.0", 0},
{"v1.99.0", "1.99.0", 0},
{"2.0.0", "1.99.9", 1},
{"1.99.0-rc1", "1.99.0", -1},
{"1.99.0", "1.99.0-rc1", 1},
{"1.99.0-rc1", "1.99.0-rc2", -1},
{"1.10.0", "1.9.0", 1}, // numeric, not lexicographic
{"garbage", "1.0.0", -1},
{"1.0.0", "garbage", 1},
{"", "", 0},
}
for _, c := range cases {
if got := compareSemver(c.a, c.b); got != c.want {
t.Errorf("compareSemver(%q, %q) = %d, want %d", c.a, c.b, got, c.want)
}
}
}
const testAsset = "AniTrack-linux-amd64"
const testData = "artifact-bytes"
type releaseSpec struct {
tag string
draft bool
prerelease bool
withAsset bool
withSidecar bool
}
// buildServer serves a canned releases index. Every spec with withAsset gets
// the test artifact; sidecars (.sha512/.sig, correctly formed) are served
// only if at least one spec sets withSidecar.
func buildServer(t *testing.T, specs []releaseSpec) *httptest.Server {
t.Helper()
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
data := []byte(testData)
sum := sha512.Sum512(data)
sha := hex.EncodeToString(sum[:]) + " " + testAsset + "\n"
sig := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, sum[:])) + "\n"
serveSidecars := false
for _, s := range specs {
if s.withSidecar {
serveSidecars = true
}
}
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(r.URL.Path, "/releases"):
w.Header().Set("Content-Type", "application/json")
var b strings.Builder
b.WriteString("[")
for i, s := range specs {
assets := ""
if s.withAsset {
assets = fmt.Sprintf(`{"name":%q,"size":%d,"browser_download_url":%q}`,
testAsset, len(data), srv.URL+"/dl/"+testAsset)
}
if i > 0 {
b.WriteString(",")
}
fmt.Fprintf(&b, `{"tag_name":%q,"name":%q,"body":"notes","draft":%v,"prerelease":%v,"published_at":"2026-09-16T00:00:00Z","assets":[%s]}`,
s.tag, s.tag, s.draft, s.prerelease, assets)
}
b.WriteString("]")
fmt.Fprint(w, b.String())
case serveSidecars && r.URL.Path == "/dl/"+testAsset+".sha512":
fmt.Fprint(w, sha)
case serveSidecars && r.URL.Path == "/dl/"+testAsset+".sig":
fmt.Fprint(w, sig)
case serveSidecars && r.URL.Path == "/dl/"+testAsset:
w.Write(data)
default:
http.NotFound(w, r)
}
}))
return srv
}
func testProvider(srv *httptest.Server, allowPre bool) *Provider {
p, err := New(Config{
BaseURL: srv.URL,
Owner: "o",
Repo: "r",
AssetName: testAsset,
AllowPrerelease: allowPre,
})
if err != nil {
panic(err)
}
return p
}
func TestCheckFindsNewest(t *testing.T) {
srv := buildServer(t, []releaseSpec{
{tag: "1.6.8", withAsset: true, withSidecar: true},
{tag: "1.99.0", withAsset: true, withSidecar: true},
})
defer srv.Close()
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
if err != nil {
t.Fatal(err)
}
if rel == nil {
t.Fatal("expected a release, got up-to-date")
}
if rel.Version != "1.99.0" {
t.Errorf("version = %q, want 1.99.0", rel.Version)
}
if rel.Verification == nil || rel.Verification.DigestAlgo != "sha512" || rel.Verification.SignatureAlgo != "ed25519ph" {
t.Errorf("verification not populated: %+v", rel.Verification)
}
if len(rel.Verification.Digest) != 64 || len(rel.Verification.Signature) == 0 {
t.Errorf("verification malformed: %+v", rel.Verification)
}
if rel.Notes != "notes" {
t.Errorf("notes = %q", rel.Notes)
}
}
func TestCheckUpToDate(t *testing.T) {
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: true}})
defer srv.Close()
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.99.0"})
if err != nil {
t.Fatal(err)
}
if rel != nil {
t.Errorf("expected up-to-date, got %s", rel.Version)
}
}
func TestCheckSkipsDraftPrereleaseAndMissingAsset(t *testing.T) {
srv := buildServer(t, []releaseSpec{
{tag: "1.99.2", draft: true, withAsset: true, withSidecar: true},
{tag: "1.99.1", prerelease: true, withAsset: true, withSidecar: true},
{tag: "1.99.0", withAsset: false},
{tag: "1.6.9", withAsset: true, withSidecar: true},
})
defer srv.Close()
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
if err != nil {
t.Fatal(err)
}
if rel == nil || rel.Version != "1.6.9" {
t.Fatalf("version = %v, want 1.6.9 (draft/prerelease/asset-less skipped)", rel)
}
}
func TestCheckAllowPrerelease(t *testing.T) {
srv := buildServer(t, []releaseSpec{
{tag: "1.99.1-rc1", prerelease: true, withAsset: true, withSidecar: true},
})
defer srv.Close()
rel, err := testProvider(srv, true).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.99.0"})
if err != nil {
t.Fatal(err)
}
if rel == nil || rel.Version != "1.99.1-rc1" {
t.Fatalf("version = %v, want 1.99.1-rc1", rel)
}
if rel.Channel != "beta" {
t.Errorf("channel = %q, want beta", rel.Channel)
}
}
func TestCheckFailsClosedWithoutSidecars(t *testing.T) {
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: false}})
defer srv.Close()
_, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
if err == nil {
t.Fatal("expected error for missing sidecars, got nil")
}
}
func TestCheckServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
}))
defer srv.Close()
_, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
if err == nil {
t.Fatal("expected error for 500, got nil")
}
}
func TestDownloadStreamsWithProgress(t *testing.T) {
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: true}})
defer srv.Close()
p := testProvider(srv, false)
rel, err := p.Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
if err != nil || rel == nil {
t.Fatalf("check: %v %v", rel, err)
}
f, err := os.CreateTemp("", "dl")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
var calls int
var last int64
err = p.Download(context.Background(), rel, f, func(written, total int64) {
calls++
last = written
if total != int64(len(testData)) {
t.Errorf("total = %d", total)
}
})
f.Close()
if err != nil {
t.Fatal(err)
}
if calls == 0 || last != int64(len(testData)) {
t.Errorf("progress calls=%d last=%d", calls, last)
}
got, _ := os.ReadFile(f.Name())
if string(got) != testData {
t.Errorf("body = %q", got)
}
}
func TestNewValidatesConfig(t *testing.T) {
if _, err := New(Config{}); err == nil {
t.Error("expected error for empty config")
}
}
// TestLiveGiteaCheck hits the real instance. Opt-in only:
// ANITRACK_LIVE_TEST=1 go test ./updater/gitea/ -run TestLiveGiteaCheck -v
func TestLiveGiteaCheck(t *testing.T) {
if os.Getenv("ANITRACK_LIVE_TEST") == "" {
t.Skip("set ANITRACK_LIVE_TEST=1 to run")
}
p, err := New(Config{
BaseURL: "https://git.linuxhg.com",
Owner: "john-okeefe",
Repo: "Anitrack",
AssetName: "AniTrack-linux-amd64",
})
if err != nil {
t.Fatal(err)
}
rel, err := p.Check(context.Background(), updater.CheckRequest{CurrentVersion: "0.0.0"})
if err != nil {
t.Fatal(err)
}
if rel == nil {
// Correct until CI publishes the bare-binary asset: no release
// carries AniTrack-linux-amd64 yet, so there is nothing to offer.
// This still proves API reachability + response parsing live.
t.Log("no matching asset published yet (expected until CI ships AniTrack-linux-amd64)")
return
}
t.Logf("latest=%s notes=%d bytes", rel.Version, len(rel.Notes))
var n int64
err = p.Download(context.Background(), rel, io.Discard, func(w, _ int64) { n = w })
if err != nil {
t.Fatal(err)
}
t.Logf("downloaded %d bytes", n)
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !production
package main
// updaterAutoCheck is false outside production builds (dev, vet, plain
// go build): the updater is compiled in but never runs.
const updaterAutoCheck = false
+140
View File
@@ -0,0 +1,140 @@
// End-to-end updater flow test (headless): Gitea fixture provider ->
// framework Check -> DownloadAndInstall -> REAL signature verification
// against a test key. Proves our sidecar shape is exactly what the
// framework verifier accepts. No network, no display, no Run().
package main
import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/updater"
giteaprovider "AniTrack/updater/gitea"
)
const flowAsset = "AniTrack-linux-amd64"
const flowData = "flow-test-binary-bytes"
// flowServer serves one signed release for version 1.99.0. When *tampered
// is true the .sig sidecar is corrupted so verification must fail. The flag
// is read per-request so one server covers both phases (the framework keeps
// a process-wide app singleton, so only one application.New per test binary).
func flowServer(t *testing.T, pub ed25519.PublicKey, priv ed25519.PrivateKey, tampered *bool) *httptest.Server {
t.Helper()
data := []byte(flowData)
sum := sha512.Sum512(data)
sigBytes, err := priv.Sign(rand.Reader, sum[:], &ed25519.Options{Hash: crypto.SHA512})
if err != nil {
t.Fatal(err)
}
sha := hex.EncodeToString(sum[:]) + " " + flowAsset + "\n"
mkSig := func() string {
s := sigBytes
if *tampered {
s = append([]byte(nil), sigBytes...)
s[0] ^= 0xff
}
return base64.StdEncoding.EncodeToString(s) + "\n"
}
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(r.URL.Path, "/releases"):
fmt.Fprintf(w, `[{"tag_name":"1.99.0","name":"1.99.0","body":"n","draft":false,"prerelease":false,"published_at":"2026-09-16T00:00:00Z","assets":[{"name":%q,"size":%d,"browser_download_url":%q}]}]`,
flowAsset, len(data), srv.URL+"/dl/"+flowAsset)
case r.URL.Path == "/dl/"+flowAsset+".sha512":
fmt.Fprint(w, sha)
case r.URL.Path == "/dl/"+flowAsset+".sig":
fmt.Fprint(w, mkSig())
case r.URL.Path == "/dl/"+flowAsset:
w.Write(data)
default:
http.NotFound(w, r)
}
}))
_ = pub
return srv
}
func TestUpdaterFlowDownloadsAndVerifies(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
_ = priv
tampered := false
srv := flowServer(t, pub, priv, &tampered)
defer srv.Close()
gh, err := giteaprovider.New(giteaprovider.Config{
BaseURL: srv.URL,
Owner: "o",
Repo: "r",
AssetName: flowAsset,
})
if err != nil {
t.Fatal(err)
}
app := application.New(application.Options{Name: "AniTrackFlowTest"})
if err := app.Updater.Init(updater.Config{
CurrentVersion: "1.6.8",
Providers: []updater.Provider{gh},
PublicKey: []byte(pub),
Window: updater.WindowNone,
}); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Phase 1: good signature -> staged bytes match the fixture.
rel, err := app.Updater.Check(ctx)
if err != nil {
t.Fatal(err)
}
if rel == nil {
t.Fatal("expected an update from 1.6.8 to 1.99.0")
}
if err := app.Updater.DownloadAndInstall(ctx); err != nil {
t.Fatalf("DownloadAndInstall (incl. real signature verify): %s", err)
}
staged := app.Updater.DownloadedPath()
if staged == "" {
t.Fatal("no staged path after install")
}
got, err := os.ReadFile(staged)
if err != nil {
t.Fatal(err)
}
if string(got) != flowData {
t.Errorf("staged bytes = %q, want fixture", got)
}
os.Remove(staged)
// Phase 2: tampered signature -> install must fail closed.
tampered = true
if _, err := app.Updater.Check(ctx); err != nil {
t.Fatal(err)
}
if err := app.Updater.DownloadAndInstall(ctx); err == nil {
t.Fatal("expected verification failure for tampered signature, got nil")
} else {
t.Logf("rejected as expected: %s", err)
}
}
+8
View File
@@ -0,0 +1,8 @@
//go:build production
package main
// updaterAutoCheck gates the startup update check to production builds:
// `wails3 build` sets -tags production, `wails3 dev` does not, so dev
// binaries in build/bin never phone home.
const updaterAutoCheck = true
+8 -7
View File
@@ -1,17 +1,18 @@
{ {
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "AniTrack", "name": "AniTrack",
"outputfilename": "AniTrack", "frontend": {
"frontend:install": "npm install", "dir": "./frontend",
"frontend:build": "npm run build", "install": "npm install",
"frontend:dev:watcher": "npm run dev", "build": "npm run build",
"frontend:dev:serverUrl": "auto", "dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"author": { "author": {
"name": "John O'Keefe", "name": "John O'Keefe",
"email": "admin@linuxhg.com" "email": "admin@linuxhg.com"
}, },
"info": { "info": {
"productName": "AniTrack", "productName": "AniTrack",
"productVersion": "1.6.8" "productVersion": "1.99.1"
} }
} }