Compare commits

..
23 Commits
Author SHA1 Message Date
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 a8df8dcce7 chore: ignore Task cache directory
Task (used by wails3 via Taskfile.yml) stores incremental-build fingerprints under .task/checksum/. Purely local cache — hashes of sources/deps, machine-generated, recreated on next wails3 dev/build. Keep it out of git like node_modules/frontend/dist/build/bin (already ignored). No files were tracked; this just stops it showing as untracked on branches that have run wails3.
2026-09-13 18:01:01 -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
John O'Keefe 2bf8d3887e fix(ci): track frontend lockfile for reproducible installs
Release / release (push) Successful in 2m42s
The blanket package-lock.json gitignore rule meant the frontend lockfile was never committed, so every release installed whatever the registry handed it (silent transitive drift) and setup-node npm caching failed outright with unable to cache dependencies. Scope the ignore rule to the repo root (/package-lock.json; no root package.json exists) and commit the freshly updated frontend lockfile: all deps at max in-range versions (autoprefixer 10.5.6 -> 10.6.0), majors intentionally deferred to the v3 frontend refresh. Verified with a green vite build before committing.
2026-09-12 22:40:21 -04:00
John O'Keefe efe45f39e7 ci(release): cache npm, Go build cache, and Wails CLI
Release / release (push) Failing after 11s
Release runs were fully cold every time: ~13s npm install, ~39s full cgo/WebKit compile, and ~34s rebuilding the Wails CLI from source on each tag. Now that the runner cache backend is reachable again, persist all three across runs. Expected total drops from ~3.5 min toward ~2.5 min with apt as the remaining floor.

- Set up Node: cache npm keyed on frontend/package-lock.json, so the npm install inside wails build stops fetching cold
- New Cache Go build cache step: setup-go only persists modules (GOMODCACHE); this persists compiled packages (GOCACHE) with a go.sum-hashed key plus a version-prefix restore-key fallback, turning the WebKit compile incremental from the second run on
- Wails CLI: split install into cache (key wails-v2.15.0-Linux, bump with the version pin), conditional go install on miss, and an unconditional PATH step so cache hits still land on PATH

First run after this still compiles cold (it saves); the payoff shows from the second run on. Verify with the throwaway-tag loop and compare Build Linux binary times.
2026-09-12 22:26:12 -04:00
John O'Keefe 336a1f353f chore(release): bump version to 1.6.8
Release / release (push) Canceled after 20s
2026-09-12 21:27:12 -04:00
John O'Keefe a19080ef39 Revert "docs: add Wails v3 + Android migration plan"
The migration plan belongs to the wailsv3 branch, not main. It was merged forward into wailsv3 (now at origin/wailsv3) before this revert, so no content is lost. Main keeps only the v2 keyring error-handling fix.
2026-09-12 21:26:25 -04:00
John O'Keefe 2bddc6a224 docs: add Wails v3 + Android migration plan
Skimmable phased plan for getting AniTrack onto Android for personal
sideload use (no Play Store, no official F-Droid). Written for both the
maintainer and as working context for AI-assisted implementation.

Covers: desktop v3 parity as a mechanical step (main.go, runtime call
mapping, wailsjs import rewrite), per-file Android storage shims
(keyring on desktop vs EncryptedSharedPreferences via
Android.Secure*), the localhost :6734 to anitrack:// deep-link OAuth
redesign with per-provider dashboard changes, and the sideload run/
package commands plus Obtainium updates. Includes guardrails: small
diffs, per-service file ownership, build-tagged platform files only,
runnable check per phase, and open decisions (first provider, desktop
parallel builds, redirect URI registration).
2026-09-12 21:23:14 -04:00
John O'Keefe 3bf5d8290e fix(auth): handle keyring open/set errors per service
Stop ignoring keyring.Open failures in all three login files. A failed
Open previously left a nil ring behind the blank identifier, so the next
Get/Set panicked with no message. Each service now opens its ring in
init, logs a service-prefixed warning when storage is unavailable, and
guards every use with a Ready check that fails safe to logged-out.

- AniListUserFunctions.go: add aniRingReady/aniRingSet helpers, log
  Open and per-key Set failures, reject invalid ExpiresIn instead of
  silently storing 0, clear in-memory JWT on logout even when storage
  is missing
- MALUserFunctions.go: add malRingReady/malRingSet helpers covering
  login, OAuth callback, and token-refresh saves; same Open/Set/
  ExpiresIn/logout treatment
- SimklUserFunctions.go: add simklRingReady/simklRingSet helpers;
  same Open/Set/logout treatment

No wallet, key names, or login flow changed. Same ServiceName
AniTrack, same keys, same OAuth callback behavior.
2026-09-12 21:22:58 -04:00
53 changed files with 4273 additions and 2797 deletions
+33 -3
View File
@@ -80,25 +80,55 @@ jobs:
go-version: '1.25'
cache-dependency-path: go.sum
- name: Cache Go build cache
uses: actions/cache@v4
with:
# setup-go only caches modules (GOMODCACHE). This caches compiled
# packages (GOCACHE) so the cgo/WebKit compile goes incremental.
# restore-keys gives a close cache instead of a cold one when
# go.sum changes. First run after adding this still compiles cold
# (it saves); the payoff shows from the second run on.
path: ~/.cache/go-build
key: go-build-1.25-${{ runner.os }}-${{ hashFiles('go.sum') }}
restore-keys: |
go-build-1.25-${{ runner.os }}-
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
# Caches npm's download cache keyed on the frontend lockfile, so
# the `npm install` inside `wails build` stops fetching cold.
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install Wails Linux build dependencies
run: |
set -euo pipefail
sudo apt-get update
# v3 desktop defaults to the GTK4/WebKitGTK 6.0 stack.
sudo apt-get install -y \
build-essential pkg-config \
libgtk-3-dev libwebkit2gtk-4.1-dev \
libgtk-4-dev libwebkitgtk-6.0-dev \
jq
- name: Cache Wails CLI
uses: actions/cache@v4
id: wails-cli
with:
# The compiled CLI binary. Bump the key whenever the @version pin
# below changes, or the old CLI will be silently reused.
path: ~/go/bin/wails3
key: wails3-v3.0.0-beta.20-${{ runner.os }}
- name: Install Wails CLI
if: steps.wails-cli.outputs.cache-hit != 'true'
run: |
set -euo pipefail
go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0
echo "${HOME}/go/bin" >> "${GITHUB_PATH}"
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.20
- name: Add Go bin to PATH
run: echo "${HOME}/go/bin" >> "${GITHUB_PATH}"
- name: Write environment.go from secrets
env:
+5 -2
View File
@@ -23,10 +23,13 @@ go.work
# ---> Wails
build/bin
.task/
node_modules
frontend/dist
package.json.md5
package-lock.json
frontend/package.json.md5
# Root-level lockfile only: the frontend lockfile IS tracked so CI installs
# are reproducible and setup-node's npm cache has something to hash.
/package-lock.json
.idea
.env
environment.go
+57 -58
View File
@@ -13,35 +13,43 @@ import (
"strings"
"sync"
"github.com/99designs/keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zalando/go-keyring"
)
var aniListJwt AniListJWT
var aniRing, _ = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
// aniKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
const aniKeyringService = "AniTrack"
func aniRingSet(key string, data []byte) error {
if err := keyring.Set(aniKeyringService, key, string(data)); err != nil {
log.Printf("anilist: save %s failed: %s", key, err)
return err
}
return nil
}
var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
func (a *App) CheckIfAniListLoggedIn() bool {
if (AniListJWT{} == aniListJwt) {
tokenType, tokenErr := aniRing.Get("anilistTokenType")
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 {
tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
return false
} else {
aniListJwt.TokenType = string(tokenType.Data)
aniListJwt.AccessToken = string(accessToken.Data)
aniListJwt.RefreshToken = string(refreshToken.Data)
aniListJwt.ExpiresIn, _ = strconv.Atoi(string(expiresIn.Data))
var expiresInConvertErr error
aniListJwt.TokenType = tokenType
aniListJwt.AccessToken = accessToken
aniListJwt.RefreshToken = refreshToken
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil {
log.Printf("anilist: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
return false
}
return true
}
} else {
@@ -51,23 +59,30 @@ func (a *App) CheckIfAniListLoggedIn() bool {
func (a *App) AniListLogin() {
if (AniListJWT{} == aniListJwt) {
tokenType, tokenErr := aniRing.Get("anilistTokenType")
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 {
tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
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.Add(1)
a.handleAniListCallback(serverDone)
serverDone.Wait()
} else {
aniListJwt.TokenType = string(tokenType.Data)
aniListJwt.AccessToken = string(accessToken.Data)
aniListJwt.RefreshToken = string(refreshToken.Data)
aniListJwt.ExpiresIn, _ = strconv.Atoi(string(expiresIn.Data))
var expiresInConvertErr error
aniListJwt.TokenType = tokenType
aniListJwt.AccessToken = accessToken
aniListJwt.RefreshToken = refreshToken
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil {
log.Printf("anilist: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
}
}
}
}
@@ -85,33 +100,17 @@ func (a *App) handleAniListCallback(wg *sync.WaitGroup) {
content := r.FormValue("code")
if content != "" {
aniListJwt = getAniListAuthorizationToken(content)
_ = aniRing.Set(keyring.Item{
Key: "anilistTokenType",
Data: []byte(aniListJwt.TokenType),
})
_ = aniRing.Set(keyring.Item{
Key: "anilistTokenExpiresIn",
Data: []byte(strconv.Itoa(aniListJwt.ExpiresIn)),
})
_ = aniRing.Set(keyring.Item{
Key: "anilistAccessToken",
Data: []byte(aniListJwt.AccessToken),
})
_ = aniRing.Set(keyring.Item{
Key: "anilistRefreshToken",
Data: []byte(aniListJwt.RefreshToken),
})
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
Title: "AniList Authorization",
Message: "It is now safe to close your browser tab",
})
if err != nil {
log.Println(err)
}
_ = aniRingSet("anilistTokenType", []byte(aniListJwt.TokenType))
_ = aniRingSet("anilistTokenExpiresIn", []byte(strconv.Itoa(aniListJwt.ExpiresIn)))
_ = aniRingSet("anilistAccessToken", []byte(aniListJwt.AccessToken))
_ = aniRingSet("anilistRefreshToken", []byte(aniListJwt.RefreshToken))
a.app.Dialog.Info().
SetTitle("AniList Authorization").
SetMessage("It is now safe to close your browser tab").
Show()
fmt.Println("Shutting down...")
aniCancel()
err = srv.Shutdown(context.Background())
if err != nil {
if err := srv.Shutdown(context.Background()); err != nil {
log.Println("server.Shutdown:", err)
}
} else {
@@ -208,12 +207,12 @@ func (a *App) GetAniListLoggedInUser() AniListUser {
func (a *App) LogoutAniList() string {
if (AniListJWT{} != aniListJwt) {
typeErr := aniRing.Remove("anilistTokenType")
expiresInErr := aniRing.Remove("anilistTokenExpiresIn")
accessTokenErr := aniRing.Remove("anilistAccessToken")
refreshTokenErr := aniRing.Remove("anilistRefreshToken")
typeErr := keyring.Delete(aniKeyringService, "anilistTokenType")
expiresInErr := keyring.Delete(aniKeyringService, "anilistTokenExpiresIn")
accessTokenErr := keyring.Delete(aniKeyringService, "anilistAccessToken")
refreshTokenErr := keyring.Delete(aniKeyringService, "anilistRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
fmt.Println("AniList Logout Failed")
log.Printf("anilist: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr)
}
aniListJwt = AniListJWT{}
}
+55 -76
View File
@@ -17,19 +17,22 @@ import (
"sync"
"time"
"github.com/99designs/keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zalando/go-keyring"
)
var myAnimeListJwt MyAnimeListJWT
var myAnimeListRing, _ = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
// malKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
const malKeyringService = "AniTrack"
func malRingSet(key string, data []byte) error {
if err := keyring.Set(malKeyringService, key, string(data)); err != nil {
log.Printf("mal: save %s failed: %s", key, err)
return err
}
return nil
}
var myAnimeListCtxShutdown, myAnimeListCancel = context.WithCancel(context.Background())
@@ -72,21 +75,22 @@ func (v *CodeVerifier) CodeChallengeS256() string {
func (a *App) CheckIfMyAnimeListLoggedIn() bool {
if (MyAnimeListJWT{} == myAnimeListJwt) {
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
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 {
tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
return false
} else {
var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data)
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil {
fmt.Println("unable to convert string to int")
log.Printf("mal: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
return false
}
myAnimeListJwt.AccessToken = string(accessToken.Data)
myAnimeListJwt.RefreshToken = string(refreshToken.Data)
myAnimeListJwt.AccessToken = accessToken
myAnimeListJwt.RefreshToken = refreshToken
return true
}
} else {
@@ -97,27 +101,30 @@ func (a *App) CheckIfMyAnimeListLoggedIn() bool {
func (a *App) MyAnimeListLogin() {
if !a.CheckIfMyAnimeListLoggedIn() {
fmt.Println("check logged in function failed")
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
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 {
tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
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"
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.Add(1)
a.handleMyAnimeListCallback(serverDone, verifier)
serverDone.Wait()
} else {
var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data)
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil {
fmt.Println("unable to convert string to int in Login function")
log.Printf("mal: invalid expiresIn %q: %s", expiresIn, expiresInConvertErr)
}
myAnimeListJwt.AccessToken = string(accessToken.Data)
myAnimeListJwt.RefreshToken = string(refreshToken.Data)
myAnimeListJwt.AccessToken = accessToken
myAnimeListJwt.RefreshToken = refreshToken
}
}
}
@@ -136,33 +143,17 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
if content != "" {
myAnimeListJwt = getMyAnimeListAuthorizationToken(content, verifier)
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListTokenType",
Data: []byte(myAnimeListJwt.TokenType),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListExpiresIn",
Data: []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListAccessToken",
Data: []byte(myAnimeListJwt.AccessToken),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListRefreshToken",
Data: []byte(myAnimeListJwt.RefreshToken),
})
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
Title: "MyAnimeList Authorization",
Message: "It is now safe to close your browser tab",
})
if err != nil {
log.Println(err)
}
_ = malRingSet("MyAnimeListTokenType", []byte(myAnimeListJwt.TokenType))
_ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)))
_ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken))
_ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken))
a.app.Dialog.Info().
SetTitle("MyAnimeList Authorization").
SetMessage("It is now safe to close your browser tab").
Show()
fmt.Println("Shutting down...")
myAnimeListCancel()
err = srv.Shutdown(context.Background())
if err != nil {
if err := srv.Shutdown(context.Background()); err != nil {
log.Println("server.Shutdown:", err)
}
} else {
@@ -292,22 +283,10 @@ func refreshMyAnimeListAuthorizationToken() bool {
myAnimeListJwt = refreshed
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListTokenType",
Data: []byte(myAnimeListJwt.TokenType),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListExpiresIn",
Data: []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListAccessToken",
Data: []byte(myAnimeListJwt.AccessToken),
})
_ = myAnimeListRing.Set(keyring.Item{
Key: "MyAnimeListRefreshToken",
Data: []byte(myAnimeListJwt.RefreshToken),
})
_ = malRingSet("MyAnimeListTokenType", []byte(myAnimeListJwt.TokenType))
_ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)))
_ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken))
_ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken))
return true
}
@@ -367,12 +346,12 @@ func createUser() MyAnimeListUser {
func (a *App) LogoutMyAnimeList() string {
if (MyAnimeListJWT{} != myAnimeListJwt) {
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType")
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn")
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken")
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken")
typeErr := keyring.Delete(malKeyringService, "MyAnimeListTokenType")
expiresInErr := keyring.Delete(malKeyringService, "MyAnimeListExpiresIn")
accessTokenErr := keyring.Delete(malKeyringService, "MyAnimeListAccessToken")
refreshTokenErr := keyring.Delete(malKeyringService, "MyAnimeListRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
fmt.Println("MAL Logout Failed")
log.Printf("mal: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr)
}
myAnimeListJwt = MyAnimeListJWT{}
}
+10 -5
View File
@@ -1,12 +1,12 @@
TAGS := webkit2_41
.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:
wails dev -tags $(TAGS)
wails3 dev -port 5173
build:
wails build -tags $(TAGS)
wails3 build
clean:
rm -rf build/bin/*
@@ -40,7 +40,12 @@ 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
@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')"
@git add wails.json
# TEMP until wails.json (v2 leftover) is retired: bump build/config.yml too.
# 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(); s2=re.sub(r'^ version: \"[^\"]*\"', ' version: \"$(VERSION)\"', s, count=1, flags=re.M); assert s2 != s, 'info.version not found'; open(p,'w').write(s2)"
@git add wails.json build/config.yml
@git commit -m "chore(release): bump version to $(VERSION)"
@echo "Generating release notes for $(VERSION)..."
@git tag "$(VERSION)" HEAD && \
+23 -6
View File
@@ -8,6 +8,12 @@ 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:** 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`).
> 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.
## Build from Source
@@ -29,15 +35,26 @@ 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.
### 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
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
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.
To run in live development mode, run `make dev` in the project directory (it runs `wails3 dev`). This starts a Vite development
server on http://localhost:5173 with very fast hot reload of your frontend changes, alongside the desktop app window.
## 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
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`.
+42 -49
View File
@@ -11,33 +11,36 @@ import (
"net/http"
"sync"
"github.com/99designs/keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zalando/go-keyring"
)
var simklJwt SimklJWT
var simklRing, _ = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
KeychainName: "AniTrack",
KeychainSynchronizable: false,
KeychainTrustApplication: true,
KeychainAccessibleWhenUnlocked: true,
})
// simklKeyringService is the OS keyring service name all AniTrack secrets
// live under (same wallet the 99designs/keyring build used).
const simklKeyringService = "AniTrack"
func simklRingSet(key string, data []byte) error {
if err := keyring.Set(simklKeyringService, key, string(data)); err != nil {
log.Printf("simkl: save %s failed: %s", key, err)
return err
}
return nil
}
var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
func (a *App) CheckIfSimklLoggedIn() bool {
if (SimklJWT{} == simklJwt) {
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 {
tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken) == 0 {
return false
} else {
simklJwt.TokenType = string(tokenType.Data)
simklJwt.AccessToken = string(accessToken.Data)
simklJwt.Scope = string(scope.Data)
simklJwt.TokenType = tokenType
simklJwt.AccessToken = accessToken
simklJwt.Scope = scope
return true
}
} else {
@@ -47,21 +50,24 @@ func (a *App) CheckIfSimklLoggedIn() bool {
func (a *App) SimklLogin() {
if !a.CheckIfSimklLoggedIn() {
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 {
tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken) == 0 {
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.Add(1)
a.handleSimklCallback(serverDone)
serverDone.Wait()
} else {
simklJwt.TokenType = string(tokenType.Data)
simklJwt.AccessToken = string(accessToken.Data)
simklJwt.Scope = string(scope.Data)
simklJwt.TokenType = tokenType
simklJwt.AccessToken = accessToken
simklJwt.Scope = scope
}
}
}
@@ -80,29 +86,16 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
if content != "" {
simklJwt = getSimklAuthorizationToken(content)
_ = simklRing.Set(keyring.Item{
Key: "SimklTokenType",
Data: []byte(simklJwt.TokenType),
})
_ = simklRing.Set(keyring.Item{
Key: "SimklAccessToken",
Data: []byte(simklJwt.AccessToken),
})
_ = simklRing.Set(keyring.Item{
Key: "SimklScope",
Data: []byte(simklJwt.Scope),
})
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
Title: "Simkl Authorization",
Message: "It is now safe to close your browser tab",
})
if err != nil {
log.Println(err)
}
_ = simklRingSet("SimklTokenType", []byte(simklJwt.TokenType))
_ = simklRingSet("SimklAccessToken", []byte(simklJwt.AccessToken))
_ = simklRingSet("SimklScope", []byte(simklJwt.Scope))
a.app.Dialog.Info().
SetTitle("Simkl Authorization").
SetMessage("It is now safe to close your browser tab").
Show()
fmt.Println("Shutting down...")
simklCancel()
err = srv.Shutdown(context.Background())
if err != nil {
if err := srv.Shutdown(context.Background()); err != nil {
log.Println("server.Shutdown:", err)
}
} else {
@@ -218,12 +211,12 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
func (a *App) LogoutSimkl() string {
if (SimklJWT{} != simklJwt) {
tokenTypeErr := simklRing.Remove("SimklTokenType")
accessTokenErr := simklRing.Remove("SimklAccessToken")
scopeErr := simklRing.Remove("SimklScope")
tokenTypeErr := keyring.Delete(simklKeyringService, "SimklTokenType")
accessTokenErr := keyring.Delete(simklKeyringService, "SimklAccessToken")
scopeErr := keyring.Delete(simklKeyringService, "SimklScope")
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil {
fmt.Println("Simkl Logout Failed")
log.Printf("simkl: logout cleanup failed (type=%v access=%v scope=%v)", tokenTypeErr, accessTokenErr, scopeErr)
}
simklJwt = SimklJWT{}
}
+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
import (
"context"
_ "embed"
"log"
"strings"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/tidwall/gjson"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed wails.json
var wailsJSON string
var wailsContext *context.Context
// App struct
type App struct {
ctx context.Context
app *application.App
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
func NewApp(app *application.App) *App {
return &App{app: app}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
version := gjson.Get(wailsJSON, "info.productVersion")
wailsContext = &ctx
runtime.WindowSetTitle(ctx, "AniTrack "+version.String())
//runtime.WindowMaximise(ctx)
// appVersion reads the product version from wails.json.
func appVersion() string {
return gjson.Get(wailsJSON, "info.productVersion").String()
}
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
var secondInstanceArgs = secondInstanceData.Args
// appTitle is the window title: "AniTrack <version>".
func appTitle() string {
return "AniTrack " + appVersion()
}
println("user opened second instance", strings.Join(secondInstanceData.Args, ","))
println("user opened second from", secondInstanceData.WorkingDirectory)
runtime.WindowUnminimise(*wailsContext)
runtime.Show(*wailsContext)
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs)
func (a *App) onSecondInstanceLaunch(data application.SecondInstanceData) {
println("user opened second instance", strings.Join(data.Args, ","))
println("user opened second from", data.WorkingDir)
if w := a.app.Window.Current(); w != nil {
w.Restore()
w.Focus()
}
go a.app.Event.Emit("launchArgs", data.Args)
}
func (a *App) ShowVersion() {
version := gjson.Get(wailsJSON, "info.productVersion")
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
Title: "Version",
Message: "AniTrack Version: " + version.String(),
})
if err != nil {
log.Println(err)
}
a.app.Dialog.Info().
SetTitle("Version").
SetMessage("AniTrack Version: " + appVersion()).
Show()
}
+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.6.8"
# 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>
+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>
+49
View File
@@ -0,0 +1,49 @@
# 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.
## 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` (on `wailsv3` — this branch's whole purpose):** real-world dogfooding (logins, watchlist sync, error modals), then merge to `main` when Wails `v3` hits stable.
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` — still `v2`, stable, ships `1.6.8` releases.
- `wailsv3``v3` desktop, unpushed until dogfooding passes, ahead of `main` by the commits above plus `2bf8d38` (`frontend/package-lock.json`) and `efe45f3`/`a19080e`/`336a1f3` from the CI/cache work. Merge `main` forward before each `v3` push.
+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
+2724
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build:dev": "vite build --minify false --mode development",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
@@ -27,6 +28,7 @@
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@wailsio/runtime": "3.0.0-beta.20",
"flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16",
"moment": "^2.30.1"
+4 -8
View File
@@ -20,11 +20,7 @@
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import {
CheckIfAniListLoggedIn,
GetMyAnimeList,
SimklGetUserWatchlist,
} from "../wailsjs/go/main/App";
import {App} from "../bindings/AniTrack";
import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte";
@@ -49,10 +45,10 @@
await CheckIfAniListLoggedInAndLoadWatchList();
}
if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w));
await App.GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
await App.SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
watchlistNeedsRefresh.set(false);
@@ -67,7 +63,7 @@
"/": Home,
"/anime/:id": wrap({
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
conditions: [async () => await CheckIfAniListLoggedIn()],
conditions: [async () => await App.CheckIfAniListLoggedIn()],
loadingComponent: Spinner,
}),
// '*': "Not Found"
+9 -18
View File
@@ -28,16 +28,7 @@
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import {
AniListDeleteEntry,
AniListUpdateEntry,
DeleteMyAnimeListEntry,
MyAnimeListUpdate,
SimklSyncEpisodes,
SimklSyncRating,
SimklSyncRemove,
SimklSyncStatus,
} from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte";
@@ -207,7 +198,7 @@
startedAt: convertDateToAniList(startedAtDate),
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) {
setApiError(
"anilist",
@@ -261,7 +252,7 @@
comments: submitData.notes,
};
await MyAnimeListUpdate(currentMalAnime, body).then(
await App.MyAnimeListUpdate(currentMalAnime, body).then(
(malAnimeReturn: MalListStatus) => {
malAnime.update((value) => {
value.my_list_status.status = malAnimeReturn.status;
@@ -317,7 +308,7 @@
try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
if (currentSimklAnime.watched_episodes_count !== submitData.episodes) {
await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
await App.SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
(value: SimklAnime) => {
AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`,
@@ -340,7 +331,7 @@
}
if (currentSimklAnime.user_rating !== submitData.rating) {
await SimklSyncRating(currentSimklAnime, submitData.rating).then(
await App.SimklSyncRating(currentSimklAnime, submitData.rating).then(
(value) => {
AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`,
@@ -363,7 +354,7 @@
}
if (currentSimklAnime.status !== submitData.status.simkl) {
await SimklSyncStatus(
await App.SimklSyncStatus(
currentSimklAnime,
submitData.status.simkl,
).then((value) => {
@@ -409,7 +400,7 @@
isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0
) {
await AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
await App.AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
AddAnimeServiceToTable({
id: `a-${currentAniListAnime.data.MediaList.mediaId}`,
title,
@@ -436,7 +427,7 @@
try {
if (malLoggedIn && currentMalAnime.id !== 0) {
await DeleteMyAnimeListEntry(currentMalAnime.id);
await App.DeleteMyAnimeListEntry(currentMalAnime.id);
AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title,
@@ -463,7 +454,7 @@
try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
await SimklSyncRemove(currentSimklAnime);
await App.SimklSyncRemove(currentSimklAnime);
AddAnimeServiceToTable({
id: `s-${currentSimklAnime.show.ids.simkl}`,
title: currentSimklAnime.show.title,
@@ -16,10 +16,10 @@
logoutOfSimkl,
serviceLoggingIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import * as runtime from "../../wailsjs/runtime";
import {Application} from "@wailsio/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser;
@@ -189,14 +189,14 @@
<button
on:click={() => {
dropdownUser();
ShowVersion();
App.ShowVersion();
}}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
>
Version
</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"
>
Exit Application
@@ -8,7 +8,7 @@
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number;
@@ -23,7 +23,7 @@
const perPageOptions = [10, 20, 50];
function ChangeWatchListPage(newPage: number) {
GetAniListUserWatchingList(newPage, perPage, sort).then((result) => {
App.GetAniListUserWatchingList(newPage, perPage, sort).then((result) => {
watchListPage.set(newPage);
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
@@ -43,7 +43,7 @@
function changeCountPerPage(
e: Event & { currentTarget: HTMLSelectElement },
): void {
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
App.GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
(result) => {
animePerPage.set(Number(e.currentTarget.value));
watchListPage.set(1);
@@ -6,7 +6,7 @@
<div class="flex justify-end">
<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 () => {
loading.set(true);
await CheckIfAniListLoggedInAndLoadWatchList();
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {AniListSearch} from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
import type {AniSearchList} from "../anilist/types/AniListTypes";
import {push} from "svelte-spa-router";
@@ -81,7 +81,7 @@
reject(new Error("AniList is taking too long to respond. Please try again."))
}, SEARCH_TIMEOUT_MS)
})
const result = await Promise.race([AniListSearch(term), timeout])
const result = await Promise.race([App.AniListSearch(term), timeout])
if (searchRequestId !== myRequest) {
return
}
+3 -3
View File
@@ -6,7 +6,7 @@
aniListWatchlist,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { MediaListSort } from "../anilist/types/AniListTypes";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
const sortTypes = [
{ value: MediaListSort.MediaId, name: "Media Id Asc" },
@@ -55,7 +55,7 @@
console.log(sort);
async function changeWatchListSort() {
const result = await GetAniListUserWatchingList(
const result = await App.GetAniListUserWatchingList(
$watchListPage,
$animePerPage,
$aniListSort,
@@ -66,7 +66,7 @@
<select
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}
on:change={() => changeWatchListSort()}
>
@@ -11,6 +11,7 @@
import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Sort from "../helperComponents/Sort.svelte";
import RefreshWatchListButton from "./RefreshWatchListButton.svelte";
let isAniListLoggedIn: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList;
@@ -26,7 +27,10 @@
>
<div class="flex justify-between items-center mb-4">
<h1 class="text-left text-xl font-bold">Your AniList WatchList</h1>
<div class="flex">
<Sort />
<RefreshWatchListButton />
</div>
</div>
<div
@@ -1,5 +1,5 @@
<script lang="ts">
import { BrowserOpenURL } from "../../wailsjs/runtime";
import {Browser} from "@wailsio/runtime";
export let id: string;
export let url = "";
@@ -25,7 +25,7 @@
<button
type="button"
class="underline underline-offset-2 px-4 py-1"
on:click={() => BrowserOpenURL(url)}>{newId}</button
on:click={() => Browser.OpenURL(url)}>{newId}</button
>
{:else}
{id}
@@ -1,9 +1,5 @@
<script lang="ts" context="module">
import {
CheckIfAniListLoggedIn,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
} from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
import {
aniListUser,
watchListPage,
@@ -29,7 +25,7 @@
export const LoadAniListUser = async () => {
try {
await GetAniListLoggedInUser().then((user) => {
await App.GetAniListLoggedInUser().then((user) => {
aniListUser.set(user);
});
} catch (err) {
@@ -46,7 +42,7 @@
export const LoadAniListWatchList = async () => {
try {
const watchList = await GetAniListUserWatchingList(page, perPage, sort);
const watchList = await App.GetAniListUserWatchingList(page, perPage, sort);
aniListWatchlist.set(watchList);
clearApiError();
} catch (err) {
@@ -63,7 +59,7 @@
export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
serviceLoggingIn.update((s) => [...s, "anilist"]);
try {
const loggedIn = await CheckIfAniListLoggedIn();
const loggedIn = await App.CheckIfAniListLoggedIn();
if (loggedIn) {
await LoadAniListUser();
if (isAniListPrimary) await LoadAniListWatchList();
@@ -1,5 +1,5 @@
<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 type { MyAnimeListUser } from "../mal/types/MALTypes";
@@ -8,9 +8,9 @@
export const CheckIfMALLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "mal"])
await CheckIfMyAnimeListLoggedIn().then(loggedIn => {
await App.CheckIfMyAnimeListLoggedIn().then(loggedIn => {
if (loggedIn) {
GetMyAnimeListLoggedInUser().then(user => {
App.GetMyAnimeListLoggedInUser().then(user => {
if (!user.name) {
malUser.set({} as MyAnimeListUser)
malLoggedIn.set(false)
@@ -18,7 +18,7 @@
}
malUser.set(user)
if (isMalPrimary) {
GetMyAnimeList(1000).then(watchList => {
App.GetMyAnimeList(1000).then(watchList => {
malWatchList.set(watchList)
malLoggedIn.set(loggedIn)
})
@@ -1,5 +1,5 @@
<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";
let isSimklPrimary: boolean
@@ -7,15 +7,15 @@
export const CheckIfSimklLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "simkl"])
await CheckIfSimklLoggedIn().then(loggedIn => {
await App.CheckIfSimklLoggedIn().then(loggedIn => {
if (loggedIn) {
GetSimklLoggedInUser().then(user => {
App.GetSimklLoggedInUser().then(user => {
if (Object.keys(user).length === 0) {
simklLoggedIn.set(false)
} else {
simklUser.set(user)
if (isSimklPrimary) {
SimklGetUserWatchlist().then(result => {
App.SimklGetUserWatchlist().then(result => {
simklWatchList.set(result)
simklLoggedIn.set(loggedIn)
})
@@ -1,17 +1,5 @@
<script lang="ts" context="module">
import {
GetAniListItem,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
GetMyAnimeListAnime,
GetMyAnimeListLoggedInUser,
GetSimklLoggedInUser,
LogoutAniList,
LogoutMyAnimeList,
LogoutSimkl,
SimklGetUserWatchlist,
SimklSearch,
} from "../../wailsjs/go/main/App";
import {App} from "../../bindings/AniTrack";
import type {
AniListCurrentUserWatchList,
AniListGetSingleAnime,
@@ -109,7 +97,7 @@
aniId: number,
login: boolean,
): Promise<""> {
await GetAniListItem(aniId, login).then((aniListResult) => {
await App.GetAniListItem(aniId, login).then((aniListResult) => {
let finalResult: AniListGetSingleAnime;
finalResult = aniListResult;
if (login === false) {
@@ -133,14 +121,14 @@
);
});
if (isMalLoggedIn) {
await GetMyAnimeListAnime(
await App.GetMyAnimeListAnime(
currentAniListAnime.data.MediaList.media.idMal,
).then((malResult) => {
malAnime.set(malResult);
});
}
if (isSimklLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then(
await App.SimklSearch(currentAniListAnime.data.MediaList).then(
(value: SimklAnime) => {
simklAnime.set(value);
},
@@ -168,13 +156,13 @@
export function loginToSimkl(): void {
setServiceLoggingIn("simkl", true);
GetSimklLoggedInUser()
App.GetSimklLoggedInUser()
.then((user) => {
if (Object.keys(user).length === 0) {
simklLoggedIn.set(false);
} else {
simklUser.set(user);
SimklGetUserWatchlist().then((result) => {
App.SimklGetUserWatchlist().then((result) => {
simklWatchList.set(result);
simklLoggedIn.set(true);
});
@@ -185,11 +173,11 @@
export function loginToAniList(): void {
setServiceLoggingIn("anilist", true);
GetAniListLoggedInUser()
App.GetAniListLoggedInUser()
.then((result) => {
aniListUser.set(result);
if (isAniListPrimary) {
GetAniListUserWatchingList(page, perPage, sort).then((result) => {
App.GetAniListUserWatchingList(page, perPage, sort).then((result) => {
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
});
@@ -202,7 +190,7 @@
export function loginToMAL(): void {
setServiceLoggingIn("mal", true);
GetMyAnimeListLoggedInUser()
App.GetMyAnimeListLoggedInUser()
.then((result) => {
if (!result.name) {
malUser.set({} as MyAnimeListUser);
@@ -216,7 +204,7 @@
}
export function logoutOfAniList(): void {
LogoutAniList().then((result) => {
App.LogoutAniList().then((result) => {
console.log(result);
if (Object.keys(aniWatchlist).length !== 0) {
aniListWatchlist.set({} as AniListCurrentUserWatchList);
@@ -227,7 +215,7 @@
}
export function logoutOfMAL(): void {
LogoutMyAnimeList().then((result) => {
App.LogoutMyAnimeList().then((result) => {
console.log(result);
malUser.set({} as MyAnimeListUser);
malLoggedIn.set(false);
@@ -235,7 +223,7 @@
}
export function logoutOfSimkl(): void {
LogoutSimkl().then((result) => {
App.LogoutSimkl().then((result) => {
console.log(result);
simklUser.set({} as SimklUser);
simklLoggedIn.set(false);
-1
View File
@@ -46,7 +46,6 @@
</div>
</div>
{:else if isAniListLoggedIn && isAniListPrimary}
<RefreshWatchListButton />
<div class="container py-10">
<Pagination />
<WatchList />
+7 -1
View File
@@ -1,7 +1,13 @@
import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'
import wails from '@wailsio/runtime/plugins/vite'
// https://vitejs.dev/config/
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
require (
github.com/99designs/keyring v1.2.2
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 (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/coder/websocket v1.8.14 // 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/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // 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-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/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/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=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
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/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
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/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/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA=
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 h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
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/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/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
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/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/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.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=
github.com/wailsapp/wails/v3 v3.0.0-beta.20 h1:AKKrRGMSqzJET1W0Jt9v+f0oOqM9Amsi7LKMM6XnSOE=
github.com/wailsapp/wails/v3 v3.0.0-beta.20/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+41 -25
View File
@@ -2,45 +2,61 @@ package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
// The service is created after the application (it needs the app
// 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
err := wails.Run(&options.App{
Title: "AniTrack",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
app := application.New(application.Options{
Name: "AniTrack",
Description: "Track anime watchlists across AniList, MyAnimeList, and Simkl",
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b",
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
SingleInstance: &application.SingleInstanceOptions{
UniqueID: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b",
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
svc.onSecondInstanceLaunch(data)
},
Bind: []interface{}{
app,
},
Linux: &linux.Options{
Icon: []byte("./build/AniTrack.png"),
WindowIsTranslucent: false,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
Linux: application.LinuxOptions{
ProgramName: "AniTrack",
},
})
if err != nil {
println("Error:", err.Error())
svc = NewApp(app)
app.RegisterService(application.NewService(svc))
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)
}
}
+8 -7
View File
@@ -1,17 +1,18 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "AniTrack",
"outputfilename": "AniTrack",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"author": {
"name": "John O'Keefe",
"email": "admin@linuxhg.com"
},
"info": {
"productName": "AniTrack",
"productVersion": "1.6.7"
"productVersion": "1.99.0"
}
}