Compare commits

..
33 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
John O'Keefe a5da544dd7 chore(release): bump version to 1.6.7
Release / release (push) Successful in 11m52s
2026-09-11 16:22:45 -04:00
John O'Keefe 270faeb507 feat(ci): automate Linux release builds and Gitea releases
Release / release (push) Successful in 13m50s
Local flow is now ./release 1.6.7 (or make release VERSION=1.6.7):
bump wails.json productVersion, commit the bump, generate git-cliff
notes into an annotated plain-version tag, and push commit plus tag.
The tag push fires the new Gitea Actions workflow.

- cliff.toml: git-cliff groups matching Bookhoard, tag_pattern for
  plain versions so --latest scopes correctly.
- Makefile release target: strict plain-semver validation, dirty-tree
  and existing-tag guards, python3 wails.json bump committed as
  chore(release), throwaway-tag notes generation, push of main + tag.
- release: wrapper normalizing v/AniTrack- prefixes to plain versions.
- .gitea/workflows/release.yml: version guard (wails.json vs tag,
  base-match for -suffix pre-releases), Go 1.25 / Node 20 / Wails
  v2.15.0 + webkit2_41 system deps, environment.go from Actions
  secrets, make build, AniTrack-<version>.tar.gz packaged from
  build/ (bin, icon, desktop, install script, README), idempotent
  Release create/update named AniTrack-<version> with archive
  attached via REGISTRY_TOKEN PAT.
2026-09-11 16:00:37 -04:00
John O'Keefe abba1803fc chore(release): bump version to 1.6.6
Update productVersion in wails.json from 1.6.5 to 1.6.6 to accompany
the MAL token variable fix and the authenticated AniList search fix.
2026-09-11 15:23:43 -04:00
John O'Keefe ce35a20eba fix(anilist): send auth token on search when logged in
AniListSearch hardcoded false for the authenticated flag to
AniListQuery, forcing anonymous searches even with a valid AniList
session. Pass a.CheckIfAniListLoggedIn() instead so the stored token
is attached when available, enabling user-specific data and
authenticated rate limits while keeping anonymous search as fallback.
2026-09-11 15:23:40 -04:00
John O'Keefe e5ce250392 fix(mal): correct swapped access and refresh token variables
In CheckIfMyAnimeListLoggedIn and MyAnimeListLogin the keyring lookups
assigned MyAnimeListAccessToken to refreshToken and
MyAnimeListRefreshToken to accessToken. Downstream code checks
len(accessToken.Data) for login state and assigns
accessToken.Data -> myAnimeListJwt.AccessToken, so the swap gated
login on the refresh token and stored each token in the wrong field.

Swap the variable names to match their keyring keys so login detection,
JWT restoration, and the empty-token fallback in MyAnimeListLogin use
the correct values.
2026-09-11 15:23:29 -04:00
john-okeefe 0cca48adb1 chore(release): bump version to 1.6.5 2026-09-03 19:12:18 -04:00
john-okeefe d2f2f8a618 fix(ui): make header search states explicit with slow-day timeouts
The search dropdown toggled blindly before the async call settled
and only handled success, so API failures left a blank dropdown
that looked like search did nothing.

- Replace toggle with explicit open/close, Escape and outside-click
  to dismiss, and disabled search button while a request is in flight
- Add isSearching/searchError/hasSearched states: loading text,
  failure panel with message plus Retry, empty prompt, and a
  no-results message for the searched term
- Guard null coverImage/title with a romaji/native fallback and a
  keyed each block; ignore stale late responses via request ids
- Tolerate slow AniList days: slow notice at 8s, 30s Promise.race
  fail-safe (backend allows 20s), and an immediate offline message
  via navigator.onLine so the UI never spins indefinitely
2026-09-03 19:09:44 -04:00
john-okeefe fcbcda7eac fix(anilist): harden AniList query transport and search errors
AniListQuery previously ignored json.Marshal/http.NewRequest errors,
logged the wrong variable on client.Do failure, and dereferenced a
nil response body, which could panic and leave callers hanging.

- Return early with user-safe messages on encode/request failures
- Add 20s http.Client timeout and nil guards for res/res.Body
- Keep logs generic so no auth material is ever printed
- Add aniListGraphQLErrorMessage/aniListSearchStatusError helpers
  mapping 429 to a rate-limit retry message, 5xx to a temporary
  outage message, and GraphQL errors[] (even on HTTP 200) to a
  surfaced message
- AniListSearch rejects empty bodies and unparseable payloads with
  actionable errors instead of failing silently downstream
2026-09-03 19:09:40 -04:00
john-okeefe 0a89ec3652 fix(anilist): decode array-shaped airingSchedule/relations and isolate per-service sync errors
The 1.6.1 fix exposed a latent decoding bug: AniList returns
airingSchedule.nodes and relations.nodes as arrays, but the Go Media
struct declared Nodes as single structs. Every full-media response
failed json.Unmarshal partway through - the update path turned that
into a rejected promise which skipped the MAL and Simkl syncs and all
table updates, while page loads silently continued with partially
decoded data (the reason the old tags/genres copy workaround existed).

- Media.AiringSchedule.Nodes is now []AiringScheduleNode and
  Media.Relations is an exported []MediaRelation (previously an
  unexported field silently dropped by encoding/json); title and
  fuzzy-date sub-structs promoted to named types.
- Regenerate wailsjs models for the new shapes.
- Anime.svelte: handleSubmit and deleteEntries now wrap each service
  in its own try/catch surfaced via setApiError/ErrorModal, so one
  service failing can no longer skip the others; removed the obsolete
  tags/genres copy workaround.
- AniListUpdateEntry/AniListDeleteEntry log HTTP status and response
  body on failure for terminal diagnostics.

Bump productVersion to 1.6.2.
2026-09-01 20:18:42 -04:00
john-okeefe 5a14863a8f fix(anilist): repair SaveMediaListEntry mutation and surface update failures
The AniListUpdateEntry mutation was mangled in 54c109a when the
standardized media field block was pasted in without the media { }
wrapper: media-level fields (idMal, title, ...) sat directly on
SaveMediaListEntry (which returns MediaList), the MediaList fields
(status, startedAt, ..., user) ended up at the Mutation root, and a
stray closing brace made the document a guaranteed 400. Because the
response status was discarded, the frontend received a zeroed
AniListGetSingleAnime and blanked the anime page after every submit,
making AniList appear logged out (and the change was never saved).

- Restore the mutation to match the tested bruno request: id/mediaId/
  userId, standard media block inside media { }, MediaList fields
  inside the selection, balanced braces.
- AniListUpdateEntry and AniListDeleteEntry now return an error on
  403/non-200/unparseable responses (mirroring
  GetAniListUserWatchingList) instead of silently returning zero
  values.
- Anime.svelte guards against replacing the page data with an empty
  response and raises the API error modal instead.

Bump productVersion to 1.6.1.
2026-09-01 19:49:47 -04:00
57 changed files with 5102 additions and 2840 deletions
+281
View File
@@ -0,0 +1,281 @@
name: Release
# Overrides the default run name (the tagged commit's message) so the Actions
# runs list shows "Release 1.6.7" instead.
run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}"
# Builds the Wails Linux binary via `make build`, packages
# AniTrack-<version>.tar.gz from build/, and creates/updates a Gitea Release
# named AniTrack-<version> with the archive attached. Triggered by a plain
# version tag push (1.6.7), or manually via workflow_dispatch with a tag for
# re-runs and secrets tests. Pushing to main does nothing, so
# work-in-progress commits never ship.
#
# Local flow: `./release 1.6.7` (or `make release VERSION=1.6.7`) bumps
# wails.json, commits the bump, creates an annotated tag carrying the
# git-cliff notes, and pushes commit + tag. This workflow verifies
# wails.json matches the tag before building.
#
# Secrets test (verifies environment.go wiring without shipping): tag the
# current bumped commit with a suffix and push, e.g.
# git tag 1.6.7-rc1 && git push origin 1.6.7-rc1
# Tags containing '-' are published as pre-releases; delete the test
# Release/tag afterwards.
on:
push:
tags:
- '[0-9]*.[0-9]*.[0-9]*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. 1.6.7)'
required: true
type: string
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
env:
# Resolve the target tag for both triggers: explicit input on manual
# dispatch, otherwise the pushed tag ref.
TAG: ${{ gitea.event.inputs.tag || gitea.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history ensures the tag annotation (the release notes) is present.
fetch-depth: 0
ref: ${{ gitea.event.inputs.tag || gitea.ref }}
- name: Guard wails.json matches tag
run: |
set -euo pipefail
: "${TAG:?TAG is required}"
# Normalize "v1.6.7" / "AniTrack-1.6.7" to plain "1.6.7" (make release
# only ever creates plain tags; this tolerates manual typos).
NORM="${TAG#v}"
NORM="${NORM#AniTrack-}"
BASE="${NORM%%-*}"
WAILS_VER="$(python3 -c "import json; print(json.load(open('wails.json'))['info']['productVersion'])")"
if [ "${NORM}" = "${BASE}" ]; then
if [ "${WAILS_VER}" != "${NORM}" ]; then
echo "::error::wails.json productVersion (${WAILS_VER}) != tag (${NORM}). Bump via ./release ${NORM} first." >&2
exit 1
fi
else
# Pre-release (e.g. 1.6.7-rc1): wails.json must match the base version.
if [ "${WAILS_VER}" != "${BASE}" ]; then
echo "::error::wails.json productVersion (${WAILS_VER}) != tag base (${BASE}). Bump via ./release ${BASE} first." >&2
exit 1
fi
fi
echo "VERSION=${NORM}" >> "${GITHUB_ENV}"
echo "Version guard passed: wails.json=${WAILS_VER} tag=${NORM}"
- name: Set up Go
uses: actions/setup-go@v5
with:
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-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/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:
ANILIST_SECRET_TOKEN: ${{ secrets.ANILIST_SECRET_TOKEN }}
ANILIST_APP_ID: ${{ secrets.ANILIST_APP_ID }}
ANILIST_APP_NAME: ${{ secrets.ANILIST_APP_NAME }}
ANILIST_CALLBACK_URI: ${{ secrets.ANILIST_CALLBACK_URI }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
SIMKL_CALLBACK_URI: ${{ secrets.SIMKL_CALLBACK_URI }}
MAL_CLIENT_ID: ${{ secrets.MAL_CLIENT_ID }}
MAL_CLIENT_SECRET: ${{ secrets.MAL_CLIENT_SECRET }}
MAL_CALLBACK_URI: ${{ secrets.MAL_CALLBACK_URI }}
run: |
set -euo pipefail
# All 10 fields come from Gitea Actions secrets (never committed).
# Values are masked in logs; do not echo them or run with set -x.
for v in ANILIST_SECRET_TOKEN ANILIST_APP_ID ANILIST_APP_NAME ANILIST_CALLBACK_URI SIMKL_CLIENT_ID SIMKL_CLIENT_SECRET SIMKL_CALLBACK_URI MAL_CLIENT_ID MAL_CLIENT_SECRET MAL_CALLBACK_URI; do
if [ -z "${!v:-}" ]; then
echo "::error::Missing secret ${v}. Add it under Settings → Secrets → Actions." >&2
exit 1
fi
done
cat > environment.go <<EOF
package main
var Environment = EnvironmentStruct{
ANILIST_SECRET_TOKEN: "${ANILIST_SECRET_TOKEN}",
ANILIST_APP_ID: "${ANILIST_APP_ID}",
ANILIST_APP_NAME: "${ANILIST_APP_NAME}",
ANILIST_CALLBACK_URI: "${ANILIST_CALLBACK_URI}",
SIMKL_CLIENT_ID: "${SIMKL_CLIENT_ID}",
SIMKL_CLIENT_SECRET: "${SIMKL_CLIENT_SECRET}",
SIMKL_CALLBACK_URI: "${SIMKL_CALLBACK_URI}",
MAL_CLIENT_ID: "${MAL_CLIENT_ID}",
MAL_CLIENT_SECRET: "${MAL_CLIENT_SECRET}",
MAL_CALLBACK_URI: "${MAL_CALLBACK_URI}",
}
EOF
echo "Wrote environment.go from secrets."
- name: Build Linux binary
run: |
set -euo pipefail
export PATH="${HOME}/go/bin:${PATH}"
make build
test -x build/bin/AniTrack || { echo "::error::build/bin/AniTrack missing after make build" >&2; exit 1; }
- name: Package release archive
run: |
set -euo pipefail
: "${VERSION:?VERSION missing from version-guard step}"
STAGE="dist/AniTrack-${VERSION}"
rm -rf dist "AniTrack-${VERSION}.tar.gz"
mkdir -p "${STAGE}/bin"
cp build/bin/AniTrack "${STAGE}/bin/"
cp -r build/icon "${STAGE}/"
cp build/AniTrack.desktop build/install_linux.sh build/README.md "${STAGE}/"
chmod +x "${STAGE}/bin/AniTrack" "${STAGE}/install_linux.sh"
tar -czf "AniTrack-${VERSION}.tar.gz" -C dist "AniTrack-${VERSION}"
tar tzf "AniTrack-${VERSION}.tar.gz"
echo "ARCHIVE=AniTrack-${VERSION}.tar.gz" >> "${GITHUB_ENV}"
- name: Create Gitea Release
env:
# REGISTRY_TOKEN is reused for release creation because Gitea's auto
# token cannot create releases on this instance. The PAT must carry
# write:repository scope. Idempotent: re-runs update an existing
# release for this tag instead of failing with 409. On any HTTP error
# the API response body is printed so a 403 names the missing scope.
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REPO: ${{ gitea.repository }}
run: |
set -euo pipefail
: "${TAG:?TAG is required}"
: "${VERSION:?VERSION missing from version-guard step}"
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
AUTH="Authorization: token ${TOKEN}"
# Release body = the annotated tag's message (the git-cliff notes).
# Manual test tags without an annotation fall back to the tag name.
BODY="$(git tag -l --format='%(contents)' "${TAG}")"
if [ -z "$(printf '%s' "${BODY}" | tr -d '[:space:]')" ]; then
BODY="${TAG}"
fi
# Tags containing a '-' (e.g. 1.6.7-rc1) are published as pre-releases.
PRE="false"; case "${TAG}" in *-*) PRE="true";; esac
PAYLOAD=$(jq -n \
--arg t "${TAG}" --arg n "AniTrack-${VERSION}" --arg b "${BODY}" --argjson p "${PRE}" \
'{tag_name:$t, name:$n, body:$b, draft:false, prerelease:$p}')
# POST/PATCH the release, surfacing Gitea's error message on failure
# (e.g. "token does not have write scope") instead of failing silently.
api_call() {
local method="$1" url="$2" resp code rbody
resp="$(curl -sS -w '\n%{http_code}' -X "${method}" \
-H "${AUTH}" -H "Content-Type: application/json" \
-d "${PAYLOAD}" "${url}")"
code="$(printf '%s' "${resp}" | tail -n1)"
rbody="$(printf '%s' "${resp}" | sed '$d')"
if [ "${code}" -ge 400 ]; then
echo "::error::Release API ${code} (${method} ${url}): ${rbody}" >&2
return 1
fi
}
EXISTING_ID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null || true)"
if [ -n "${EXISTING_ID}" ]; then
api_call PATCH "${API}/${EXISTING_ID}"
echo "Updated existing release id=${EXISTING_ID} for ${TAG}"
else
api_call POST "${API}"
echo "Created new release for ${TAG}"
fi
- name: Upload release archive
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REPO: ${{ gitea.repository }}
run: |
set -euo pipefail
: "${TAG:?TAG is required}"
: "${ARCHIVE:?ARCHIVE missing from packaging step}"
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
AUTH="Authorization: token ${TOKEN}"
test -f "${ARCHIVE}" || { echo "::error::${ARCHIVE} not found" >&2; exit 1; }
RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')"
if [ -z "${RID}" ]; then
echo "::error::No release found for tag ${TAG} after create step" >&2
exit 1
fi
# Replace a same-named asset so re-runs stay idempotent.
AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ARCHIVE}" '.[] | select(.name==$n) | .id // empty')"
if [ -n "${AID}" ]; then
curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null
echo "Deleted existing asset id=${AID} (${ARCHIVE})"
fi
resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \
-F "attachment=@${ARCHIVE}" "${API}/${RID}/assets?name=${ARCHIVE}")"
code="$(printf '%s' "${resp}" | tail -n1)"
rbody="$(printf '%s' "${resp}" | sed '$d')"
if [ "${code}" -ge 400 ]; then
echo "::error::Asset upload ${code}: ${rbody}" >&2
exit 1
fi
echo "Uploaded ${ARCHIVE} to release id=${RID}"
+5 -2
View File
@@ -23,10 +23,13 @@ go.work
# ---> Wails # ---> Wails
build/bin build/bin
.task/
node_modules node_modules
frontend/dist frontend/dist
package.json.md5 frontend/package.json.md5
package-lock.json # 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 .idea
.env .env
environment.go environment.go
+202 -89
View File
@@ -7,26 +7,42 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"strings"
"time"
) )
func AniListQuery(body interface{}, login bool) (json.RawMessage, string) { func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
reader, _ := json.Marshal(body) reader, err := json.Marshal(body)
response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader))
if err != nil { if err != nil {
log.Printf("Failed at response, %s\n", err) log.Printf("AniList request failed: could not encode request body\n")
return nil, "Could not prepare the AniList request."
}
request, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader))
if err != nil {
log.Printf("AniList request failed: could not create request\n")
return nil, "Could not reach AniList. Please check your connection and try again."
} }
if login && (AniListJWT{}) != aniListJwt { if login && (AniListJWT{}) != aniListJwt {
response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken) request.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
} else if login { } else if login {
return nil, "Please login to AniList to make this request" return nil, "Please login to AniList to make this request"
} }
response.Header.Add("Content-Type", "application/json") request.Header.Add("Content-Type", "application/json")
response.Header.Add("Accept", "application/json") request.Header.Add("Accept", "application/json")
client := &http.Client{} client := &http.Client{Timeout: 20 * time.Second}
res, resErr := client.Do(response) res, resErr := client.Do(request)
if resErr != nil { if resErr != nil {
log.Printf("Failed at res, %s\n", err) log.Printf("AniList request failed: network error\n")
return nil, "Could not reach AniList. Please check your connection and try again."
}
if res == nil {
log.Printf("AniList request failed: empty response\n")
return nil, "Could not reach AniList. Please check your connection and try again."
}
if res.Body == nil {
log.Printf("AniList request failed: empty response body\n")
return nil, "Could not reach AniList. Please check your connection and try again."
} }
defer res.Body.Close() defer res.Body.Close()
@@ -39,6 +55,37 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
return returnedBody, res.Status return returnedBody, res.Status
} }
func aniListGraphQLErrorMessage(returnedBody json.RawMessage) string {
var gqlErr struct {
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}
if err := json.Unmarshal(returnedBody, &gqlErr); err != nil {
return ""
}
if len(gqlErr.Errors) > 0 && strings.TrimSpace(gqlErr.Errors[0].Message) != "" {
return strings.TrimSpace(gqlErr.Errors[0].Message)
}
return ""
}
func aniListSearchStatusError(status string, returnedBody json.RawMessage) error {
if msg := aniListGraphQLErrorMessage(returnedBody); msg != "" {
return fmt.Errorf("AniList search failed: %s", msg)
}
if strings.Contains(status, "429") {
return fmt.Errorf("AniList is rate-limiting search right now. Please wait a moment and try again.")
}
if strings.Contains(status, "500") || strings.Contains(status, "502") || strings.Contains(status, "503") || strings.Contains(status, "504") {
return fmt.Errorf("AniList is temporarily unavailable (%s). Please try again shortly.", status)
}
if strings.HasPrefix(status, "Could not") || strings.HasPrefix(status, "Please login") {
return fmt.Errorf("%s", status)
}
return fmt.Errorf("AniList search failed (%s). Please try again.", status)
}
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime { func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
user := a.GetAniListLoggedInUser() user := a.GetAniListLoggedInUser()
@@ -336,14 +383,20 @@ func (a *App) AniListSearch(query string) (interface{}, error) {
ListType: "ANIME", ListType: "ANIME",
}, },
} }
returnedBody, status := AniListQuery(body, false) returnedBody, status := AniListQuery(body, a.CheckIfAniListLoggedIn())
if status != "200 OK" { if status != "200 OK" {
return nil, fmt.Errorf("API search failed with status: %s", status) return nil, aniListSearchStatusError(status, returnedBody)
}
if len(returnedBody) == 0 {
return nil, fmt.Errorf("AniList returned an empty response. Please try again.")
}
if msg := aniListGraphQLErrorMessage(returnedBody); msg != "" {
return nil, fmt.Errorf("AniList search failed: %s", msg)
} }
var post interface{} var post interface{}
err := json.Unmarshal(returnedBody, &post) err := json.Unmarshal(returnedBody, &post)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal search results\n")
return nil, fmt.Errorf("Failed to parse search results") return nil, fmt.Errorf("Failed to parse search results")
} }
return post, nil return post, nil
@@ -558,7 +611,7 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (An
return post, nil return post, nil
} }
func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime { func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) (AniListGetSingleAnime, error) {
body := struct { body := struct {
Query string `json:"query"` Query string `json:"query"`
Variables AniListUpdateVariables `json:"variables"` Variables AniListUpdateVariables `json:"variables"`
@@ -584,84 +637,87 @@ func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSi
startedAt: $startedAt startedAt: $startedAt
completedAt: $completedAt completedAt: $completedAt
) { ) {
id id
idMal mediaId
title { userId
userPreferred media {
romaji
english
native
}
description
coverImage {
extraLarge
large
medium
color
}
bannerImage
format
season
seasonYear
status
episodes
duration
countryOfOrigin
source
synonyms
averageScore
meanScore
popularity
trending
favourites
isFavourite
relations {
nodes {
id id
idMal
title { title {
userPreferred userPreferred
romaji romaji
english english
native native
} }
description
coverImage {
extraLarge
large
medium
color
}
bannerImage
format
season
seasonYear
status
episodes
duration
countryOfOrigin
source
synonyms
averageScore
meanScore
popularity
trending
favourites
isFavourite
relations {
nodes {
id
title {
userPreferred
romaji
english
native
}
}
}
startDate {
year
month
day
}
endDate {
year
month
day
}
nextAiringEpisode {
airingAt
timeUntilAiring
episode
}
airingSchedule {
nodes {
id
airingAt
timeUntilAiring
episode
mediaId
}
}
genres
tags {
id
name
description
rank
isMediaSpoiler
isAdult
}
isAdult
} }
}
startDate {
year
month
day
}
endDate {
year
month
day
}
nextAiringEpisode {
airingAt
timeUntilAiring
episode
}
airingSchedule {
nodes {
id
airingAt
timeUntilAiring
episode
mediaId
}
}
genres
tags {
id
name
description
rank
isMediaSpoiler
isAdult
}
isAdult
}
status status
startedAt { startedAt {
year year
@@ -700,22 +756,53 @@ func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSi
Variables: updateBody, Variables: updateBody,
} }
returnedBody, _ := AniListQuery(body, true) returnedBody, status := AniListQuery(body, true)
var badPost struct {
Errors []struct {
Message string `json:"message"`
Status int `json:"status"`
Locations []struct {
Line int `json:"line"`
Column int `json:"column"`
} `json:"locations"`
} `json:"errors"`
Data any `json:"data"`
}
if status == "403 Forbidden" {
err := json.Unmarshal(returnedBody, &badPost)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return AniListGetSingleAnime{}, fmt.Errorf("API authentication error")
}
return AniListGetSingleAnime{}, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message)
}
if status != "200 OK" {
log.Printf("AniListUpdateEntry failed with status: %s, body: %s\n", status, string(returnedBody))
return AniListGetSingleAnime{}, fmt.Errorf("API request failed with status: %s", status)
}
var returnedJson AniListUpdateReturn var returnedJson AniListUpdateReturn
err := json.Unmarshal(returnedBody, &returnedJson) err := json.Unmarshal(returnedBody, &returnedJson)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("AniListUpdateEntry failed to unmarshal response: %s, body: %s\n", err, string(returnedBody))
return AniListGetSingleAnime{}, fmt.Errorf("failed to parse AniList update response")
}
if returnedJson.Data.SaveMediaListEntry.MediaID == 0 {
log.Printf("AniListUpdateEntry returned no saved entry, body: %s\n", string(returnedBody))
return AniListGetSingleAnime{}, fmt.Errorf("AniList returned no saved entry")
} }
var post AniListGetSingleAnime var post AniListGetSingleAnime
post.Data.MediaList = returnedJson.Data.SaveMediaListEntry post.Data.MediaList = returnedJson.Data.SaveMediaListEntry
return post return post, nil
} }
func (a *App) AniListDeleteEntry(mediaListId int) DeleteAniListReturn { func (a *App) AniListDeleteEntry(mediaListId int) (DeleteAniListReturn, error) {
type Variables = struct { type Variables = struct {
Id int `json:"id"` Id int `json:"id"`
} }
@@ -740,15 +827,41 @@ func (a *App) AniListDeleteEntry(mediaListId int) DeleteAniListReturn {
}, },
} }
returnedBody, _ := AniListQuery(body, true) returnedBody, status := AniListQuery(body, true)
var badPost struct {
Errors []struct {
Message string `json:"message"`
Status int `json:"status"`
Locations []struct {
Line int `json:"line"`
Column int `json:"column"`
} `json:"locations"`
} `json:"errors"`
Data any `json:"data"`
}
if status == "403 Forbidden" {
err := json.Unmarshal(returnedBody, &badPost)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return DeleteAniListReturn{}, fmt.Errorf("API authentication error")
}
return DeleteAniListReturn{}, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message)
}
if status != "200 OK" {
log.Printf("AniListDeleteEntry failed with status: %s, body: %s\n", status, string(returnedBody))
return DeleteAniListReturn{}, fmt.Errorf("API request failed with status: %s", status)
}
var post DeleteAniListReturn var post DeleteAniListReturn
err := json.Unmarshal(returnedBody, &post) err := json.Unmarshal(returnedBody, &post)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return DeleteAniListReturn{}, fmt.Errorf("failed to parse AniList delete response")
} }
return post return post, nil
} }
func (a *App) AniListBrowse( func (a *App) AniListBrowse(
+60 -57
View File
@@ -64,74 +64,43 @@ type AniListUpdateReturn struct {
} }
type Media struct { type Media struct {
ID int `json:"id"` ID int `json:"id"`
IDMal int `json:"idMal"` IDMal int `json:"idMal"`
Title struct { Title MediaTitle `json:"title"`
UserPreferred string `json:"userPreferred"` Description string `json:"description"`
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
} `json:"title"`
Description string `json:"description"`
CoverImage struct { CoverImage struct {
ExtraLarge string ExtraLarge string
Large string `json:"large"` Large string `json:"large"`
Medium string Medium string
Color string Color string
} `json:"coverImage"` } `json:"coverImage"`
BannerImage string BannerImage string
Format string Format string
Season string `json:"season"` Season string `json:"season"`
SeasonYear int `json:"seasonYear"` SeasonYear int `json:"seasonYear"`
Status string `json:"status"` Status string `json:"status"`
Episodes int `json:"episodes"` Episodes int `json:"episodes"`
Duration int Duration int
CountryOfOrigin string CountryOfOrigin string
Source string Source string
Synonyms []string Synonyms []string
AverageScore int AverageScore int
MeanScore int MeanScore int
Popularity int Popularity int
Trending int Trending int
Favourites int Favourites int
isFavourite bool isFavourite bool
Relations struct { Relations MediaRelations `json:"relations"`
nodes struct { StartDate MediaFuzzyDate `json:"startDate"`
id int EndDate MediaFuzzyDate `json:"endDate"`
Title struct {
UserPreferred string `json:"userPreferred"`
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
} `json:"title"`
}
}
StartDate struct {
Year int
Month int
Day int
}
EndDate struct {
Year int
Month int
Day int
}
NextAiringEpisode struct { NextAiringEpisode struct {
AiringAt int `json:"airingAt"` AiringAt int `json:"airingAt"`
TimeUntilAiring int `json:"timeUntilAiring"` TimeUntilAiring int `json:"timeUntilAiring"`
Episode int `json:"episode"` Episode int `json:"episode"`
} `json:"nextAiringEpisode"` } `json:"nextAiringEpisode"`
AiringSchedule struct { AiringSchedule MediaAiringSchedule `json:"airingSchedule"`
Nodes struct { Genres []string `json:"genres"`
Id int Tags []struct {
AiringAt int
TimeUntilAiring int
Episode int
MediaId int
}
}
Genres []string `json:"genres"`
Tags []struct {
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
@@ -142,6 +111,40 @@ type Media struct {
IsAdult bool `json:"isAdult"` IsAdult bool `json:"isAdult"`
} }
type MediaTitle struct {
UserPreferred string `json:"userPreferred"`
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
}
type MediaRelations struct {
Nodes []MediaRelation `json:"nodes"`
}
type MediaRelation struct {
Id int `json:"id"`
Title MediaTitle `json:"title"`
}
type MediaFuzzyDate struct {
Year int `json:"year"`
Month int `json:"month"`
Day int `json:"day"`
}
type MediaAiringSchedule struct {
Nodes []AiringScheduleNode `json:"nodes"`
}
type AiringScheduleNode struct {
Id int `json:"id"`
AiringAt int `json:"airingAt"`
TimeUntilAiring int `json:"timeUntilAiring"`
Episode int `json:"episode"`
MediaId int `json:"mediaId"`
}
type MediaList struct { type MediaList struct {
ID int `json:"id"` ID int `json:"id"`
MediaID int `json:"mediaId"` MediaID int `json:"mediaId"`
+57 -58
View File
@@ -13,35 +13,43 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var aniListJwt AniListJWT var aniListJwt AniListJWT
var aniRing, _ = keyring.Open(keyring.Config{ // aniKeyringService is the OS keyring service name all AniTrack secrets
ServiceName: "AniTrack", // live under (same wallet the 99designs/keyring build used).
KeychainName: "AniTrack", const aniKeyringService = "AniTrack"
KeychainSynchronizable: false,
KeychainTrustApplication: true, func aniRingSet(key string, data []byte) error {
KeychainAccessibleWhenUnlocked: true, 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()) var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
func (a *App) CheckIfAniListLoggedIn() bool { func (a *App) CheckIfAniListLoggedIn() bool {
if (AniListJWT{} == aniListJwt) { if (AniListJWT{} == aniListJwt) {
tokenType, tokenErr := aniRing.Get("anilistTokenType") tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn") expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken") refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken") accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 { if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
return false return false
} else { } else {
aniListJwt.TokenType = string(tokenType.Data) var expiresInConvertErr error
aniListJwt.AccessToken = string(accessToken.Data) aniListJwt.TokenType = tokenType
aniListJwt.RefreshToken = string(refreshToken.Data) aniListJwt.AccessToken = accessToken
aniListJwt.ExpiresIn, _ = strconv.Atoi(string(expiresIn.Data)) 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 return true
} }
} else { } else {
@@ -51,23 +59,30 @@ func (a *App) CheckIfAniListLoggedIn() bool {
func (a *App) AniListLogin() { func (a *App) AniListLogin() {
if (AniListJWT{} == aniListJwt) { if (AniListJWT{} == aniListJwt) {
tokenType, tokenErr := aniRing.Get("anilistTokenType") tokenType, tokenErr := keyring.Get(aniKeyringService, "anilistTokenType")
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn") expiresIn, expiresInErr := keyring.Get(aniKeyringService, "anilistTokenExpiresIn")
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken") refreshToken, refreshTokenErr := keyring.Get(aniKeyringService, "anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken") accessToken, accessTokenErr := keyring.Get(aniKeyringService, "anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 { 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" getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code"
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl) if err := a.app.Browser.OpenURL(getAniListCodeUrl); err != nil {
log.Printf("anilist: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
a.handleAniListCallback(serverDone) a.handleAniListCallback(serverDone)
serverDone.Wait() serverDone.Wait()
} else { } else {
aniListJwt.TokenType = string(tokenType.Data) var expiresInConvertErr error
aniListJwt.AccessToken = string(accessToken.Data) aniListJwt.TokenType = tokenType
aniListJwt.RefreshToken = string(refreshToken.Data) aniListJwt.AccessToken = accessToken
aniListJwt.ExpiresIn, _ = strconv.Atoi(string(expiresIn.Data)) 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") content := r.FormValue("code")
if content != "" { if content != "" {
aniListJwt = getAniListAuthorizationToken(content) aniListJwt = getAniListAuthorizationToken(content)
_ = aniRing.Set(keyring.Item{ _ = aniRingSet("anilistTokenType", []byte(aniListJwt.TokenType))
Key: "anilistTokenType", _ = aniRingSet("anilistTokenExpiresIn", []byte(strconv.Itoa(aniListJwt.ExpiresIn)))
Data: []byte(aniListJwt.TokenType), _ = aniRingSet("anilistAccessToken", []byte(aniListJwt.AccessToken))
}) _ = aniRingSet("anilistRefreshToken", []byte(aniListJwt.RefreshToken))
_ = aniRing.Set(keyring.Item{ a.app.Dialog.Info().
Key: "anilistTokenExpiresIn", SetTitle("AniList Authorization").
Data: []byte(strconv.Itoa(aniListJwt.ExpiresIn)), SetMessage("It is now safe to close your browser tab").
}) Show()
_ = 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)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
aniCancel() aniCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -208,12 +207,12 @@ func (a *App) GetAniListLoggedInUser() AniListUser {
func (a *App) LogoutAniList() string { func (a *App) LogoutAniList() string {
if (AniListJWT{} != aniListJwt) { if (AniListJWT{} != aniListJwt) {
typeErr := aniRing.Remove("anilistTokenType") typeErr := keyring.Delete(aniKeyringService, "anilistTokenType")
expiresInErr := aniRing.Remove("anilistTokenExpiresIn") expiresInErr := keyring.Delete(aniKeyringService, "anilistTokenExpiresIn")
accessTokenErr := aniRing.Remove("anilistAccessToken") accessTokenErr := keyring.Delete(aniKeyringService, "anilistAccessToken")
refreshTokenErr := aniRing.Remove("anilistRefreshToken") refreshTokenErr := keyring.Delete(aniKeyringService, "anilistRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil { 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{} aniListJwt = AniListJWT{}
} }
+55 -76
View File
@@ -17,19 +17,22 @@ import (
"sync" "sync"
"time" "time"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var myAnimeListJwt MyAnimeListJWT var myAnimeListJwt MyAnimeListJWT
var myAnimeListRing, _ = keyring.Open(keyring.Config{ // malKeyringService is the OS keyring service name all AniTrack secrets
ServiceName: "AniTrack", // live under (same wallet the 99designs/keyring build used).
KeychainName: "AniTrack", const malKeyringService = "AniTrack"
KeychainSynchronizable: false,
KeychainTrustApplication: true, func malRingSet(key string, data []byte) error {
KeychainAccessibleWhenUnlocked: true, 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()) var myAnimeListCtxShutdown, myAnimeListCancel = context.WithCancel(context.Background())
@@ -72,21 +75,22 @@ func (v *CodeVerifier) CodeChallengeS256() string {
func (a *App) CheckIfMyAnimeListLoggedIn() bool { func (a *App) CheckIfMyAnimeListLoggedIn() bool {
if (MyAnimeListJWT{} == myAnimeListJwt) { if (MyAnimeListJWT{} == myAnimeListJwt) {
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType") tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn") expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken") accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken") refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 { if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
return false return false
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
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.AccessToken = accessToken
myAnimeListJwt.RefreshToken = string(refreshToken.Data) myAnimeListJwt.RefreshToken = refreshToken
return true return true
} }
} else { } else {
@@ -97,27 +101,30 @@ func (a *App) CheckIfMyAnimeListLoggedIn() bool {
func (a *App) MyAnimeListLogin() { func (a *App) MyAnimeListLogin() {
if !a.CheckIfMyAnimeListLoggedIn() { if !a.CheckIfMyAnimeListLoggedIn() {
fmt.Println("check logged in function failed") fmt.Println("check logged in function failed")
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType") tokenType, tokenErr := keyring.Get(malKeyringService, "MyAnimeListTokenType")
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn") expiresIn, expiresInErr := keyring.Get(malKeyringService, "MyAnimeListExpiresIn")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken") accessToken, accessTokenErr := keyring.Get(malKeyringService, "MyAnimeListAccessToken")
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken") refreshToken, refreshTokenErr := keyring.Get(malKeyringService, "MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 { if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken) == 0 {
verifier, _ := verifier() verifier, _ := verifier()
getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain" getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain"
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl) if err := a.app.Browser.OpenURL(getMyAnimeListCodeUrl); err != nil {
log.Printf("mal: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
a.handleMyAnimeListCallback(serverDone, verifier) a.handleMyAnimeListCallback(serverDone, verifier)
serverDone.Wait() serverDone.Wait()
} else { } else {
var expiresInConvertErr error var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.TokenType = tokenType
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data)) myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(expiresIn)
if expiresInConvertErr != nil { if expiresInConvertErr != nil {
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.AccessToken = accessToken
myAnimeListJwt.RefreshToken = string(refreshToken.Data) myAnimeListJwt.RefreshToken = refreshToken
} }
} }
} }
@@ -136,33 +143,17 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
if content != "" { if content != "" {
myAnimeListJwt = getMyAnimeListAuthorizationToken(content, verifier) myAnimeListJwt = getMyAnimeListAuthorizationToken(content, verifier)
_ = myAnimeListRing.Set(keyring.Item{ _ = malRingSet("MyAnimeListTokenType", []byte(myAnimeListJwt.TokenType))
Key: "MyAnimeListTokenType", _ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)))
Data: []byte(myAnimeListJwt.TokenType), _ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken))
}) _ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken))
_ = myAnimeListRing.Set(keyring.Item{ a.app.Dialog.Info().
Key: "MyAnimeListExpiresIn", SetTitle("MyAnimeList Authorization").
Data: []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)), SetMessage("It is now safe to close your browser tab").
}) Show()
_ = 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)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
myAnimeListCancel() myAnimeListCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -292,22 +283,10 @@ func refreshMyAnimeListAuthorizationToken() bool {
myAnimeListJwt = refreshed myAnimeListJwt = refreshed
_ = myAnimeListRing.Set(keyring.Item{ _ = malRingSet("MyAnimeListTokenType", []byte(myAnimeListJwt.TokenType))
Key: "MyAnimeListTokenType", _ = malRingSet("MyAnimeListExpiresIn", []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)))
Data: []byte(myAnimeListJwt.TokenType), _ = malRingSet("MyAnimeListAccessToken", []byte(myAnimeListJwt.AccessToken))
}) _ = malRingSet("MyAnimeListRefreshToken", []byte(myAnimeListJwt.RefreshToken))
_ = 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),
})
return true return true
} }
@@ -367,12 +346,12 @@ func createUser() MyAnimeListUser {
func (a *App) LogoutMyAnimeList() string { func (a *App) LogoutMyAnimeList() string {
if (MyAnimeListJWT{} != myAnimeListJwt) { if (MyAnimeListJWT{} != myAnimeListJwt) {
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType") typeErr := keyring.Delete(malKeyringService, "MyAnimeListTokenType")
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn") expiresInErr := keyring.Delete(malKeyringService, "MyAnimeListExpiresIn")
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken") accessTokenErr := keyring.Delete(malKeyringService, "MyAnimeListAccessToken")
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken") refreshTokenErr := keyring.Delete(malKeyringService, "MyAnimeListRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil { 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{} myAnimeListJwt = MyAnimeListJWT{}
} }
+49 -5
View File
@@ -1,12 +1,56 @@
TAGS := webkit2_41 .PHONY: dev build clean release
.PHONY: dev build clean
# v3 uses the GTK4/WebKitGTK 6.0 stack by default (same 2.52.x engine
# generation as the v2 webkit2_41 build), so no build tags are needed.
dev: dev:
wails dev -tags $(TAGS) wails3 dev -port 5173
build: build:
wails build -tags $(TAGS) wails3 build
clean: clean:
rm -rf build/bin/* rm -rf build/bin/*
# Create a version commit (wails.json bump, committed and pushed) plus an
# annotated version tag carrying auto-generated release notes (git-cliff),
# and push both. The tag push triggers .gitea/workflows/release.yml, which
# verifies wails.json matches the tag, builds via `make build`, packages
# AniTrack-<VERSION>.tar.gz from build/, and publishes a Gitea Release
# named AniTrack-<VERSION> with the archive attached. Notes come entirely
# from Conventional Commits — no hand-written message required.
#
# git-cliff's --latest needs the tag to exist to scope the notes, so we
# create a throwaway lightweight tag, generate the notes, replace it with
# an annotated tag, then push. --cleanup=verbatim keeps the markdown "###"
# group headers (git's default cleanup would strip lines starting with "#").
#
# Tags are plain versions (1.6.7) to match wails.json productVersion and the
# existing tag history. Pre-releases are created by tagging manually with a
# suffix (e.g. 1.6.7-rc1 on top of the bumped commit) — the workflow marks
# those as prerelease. Use ./release as a shortcut.
#
# Requires git-cliff: https://git-cliff.org/install
# Usage: make release VERSION=1.6.7
release:
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=1.6.7"; exit 1; }
@echo "$(VERSION)" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$$' || { echo "VERSION must be plain semver like 1.6.7 (no v prefix, no AniTrack- prefix, no suffix)"; exit 1; }
@command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; }
@command -v python3 >/dev/null 2>&1 || { echo "python3 not found — required to bump wails.json"; exit 1; }
@git diff --quiet && git diff --cached --quiet || { echo "Working tree dirty — commit or stash first"; exit 1; }
@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')"
# 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 && \
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
{ git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; }
@git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp
@git push origin main "$(VERSION)"
@echo "Pushed $(VERSION) — Gitea Actions will build, package, and publish AniTrack-$(VERSION).tar.gz."
+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 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. If you are getting too many errors due to api usage, please build from source.
## 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. Once you have the IDs, Keys, and Secrets create an environment.go file based on the environment.go.example and fill in the fields.
### Install Wails and Dependencies ### Install Wails and Dependencies
Please follow the instructions [here](https://wails.io/docs/gettingstarted/installation) to get Wails up and running and follow the instructions below. Please follow the instructions [here](https://v3.wails.io/getting-started/your-first-app/) to get Wails v3 up and running and follow the instructions below.
System packages for building (the WebKitGTK 6 runtime above, plus
compilers and headers) — Arch: `sudo pacman -S base-devel gtk4 webkitgtk-6.0 go nodejs npm` ·
Debian/Ubuntu: `sudo apt install build-essential pkg-config libgtk-4-dev libwebkitgtk-6.0-dev golang nodejs npm`,
then install the pinned CLI with `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.20`.
> **Versioning note:** `1.99.x` is the v3-migration beta series — Wails v3
> underneath, Svelte 4/Vite 4 toolchain refresh still pending. `2.0.0` is
> reserved for the finished article. See `docs/V3_MIGRATION.md`.
## Live Development ## Live Development
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development To run in live development mode, run `make dev` in the project directory (it runs `wails3 dev`). This starts a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser server on http://localhost:5173 with very fast hot reload of your frontend changes, alongside the desktop app window.
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools. ## Migrating from v2
The `wailsv3` branch moves AniTrack from Wails `v2``v3` and from `99designs/keyring``zalando/go-keyring`. Both versions can coexist: on Linux, `v2` tokens live in an `AniTrack` Secret Service collection while `v3` uses the `login` collection, so one does not overwrite the other. After you log into AniList / MAL / Simkl once on `v3`, the `v2` entries become stale but harmless orphans — leave them if you still run both versions, or clear the old collection when youve fully moved over. See `docs/V3_MIGRATION.md` for the full migration summary and next steps (including planned Android support).
## Building ## Building
To build a redistributable, production mode package, use `wails build --clean`. To build a redistributable, production mode package, use `make build`. The binary lands at `build/bin/AniTrack`.
+42 -49
View File
@@ -11,33 +11,36 @@ import (
"net/http" "net/http"
"sync" "sync"
"github.com/99designs/keyring" "github.com/zalando/go-keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
) )
var simklJwt SimklJWT var simklJwt SimklJWT
var simklRing, _ = keyring.Open(keyring.Config{ // simklKeyringService is the OS keyring service name all AniTrack secrets
ServiceName: "AniTrack", // live under (same wallet the 99designs/keyring build used).
KeychainName: "AniTrack", const simklKeyringService = "AniTrack"
KeychainSynchronizable: false,
KeychainTrustApplication: true, func simklRingSet(key string, data []byte) error {
KeychainAccessibleWhenUnlocked: true, 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()) var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
func (a *App) CheckIfSimklLoggedIn() bool { func (a *App) CheckIfSimklLoggedIn() bool {
if (SimklJWT{} == simklJwt) { if (SimklJWT{} == simklJwt) {
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType") tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken") accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope") scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 { if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken) == 0 {
return false return false
} else { } else {
simklJwt.TokenType = string(tokenType.Data) simklJwt.TokenType = tokenType
simklJwt.AccessToken = string(accessToken.Data) simklJwt.AccessToken = accessToken
simklJwt.Scope = string(scope.Data) simklJwt.Scope = scope
return true return true
} }
} else { } else {
@@ -47,21 +50,24 @@ func (a *App) CheckIfSimklLoggedIn() bool {
func (a *App) SimklLogin() { func (a *App) SimklLogin() {
if !a.CheckIfSimklLoggedIn() { if !a.CheckIfSimklLoggedIn() {
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType") tokenType, tokenTypeErr := keyring.Get(simklKeyringService, "SimklTokenType")
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken") accessToken, accessTokenErr := keyring.Get(simklKeyringService, "SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope") scope, scopeErr := keyring.Get(simklKeyringService, "SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 { 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 getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl) if err := a.app.Browser.OpenURL(getSimklCodeUrl); err != nil {
log.Printf("simkl: failed to open browser: %s", err)
return
}
serverDone := &sync.WaitGroup{} serverDone := &sync.WaitGroup{}
serverDone.Add(1) serverDone.Add(1)
a.handleSimklCallback(serverDone) a.handleSimklCallback(serverDone)
serverDone.Wait() serverDone.Wait()
} else { } else {
simklJwt.TokenType = string(tokenType.Data) simklJwt.TokenType = tokenType
simklJwt.AccessToken = string(accessToken.Data) simklJwt.AccessToken = accessToken
simklJwt.Scope = string(scope.Data) simklJwt.Scope = scope
} }
} }
} }
@@ -80,29 +86,16 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
if content != "" { if content != "" {
simklJwt = getSimklAuthorizationToken(content) simklJwt = getSimklAuthorizationToken(content)
_ = simklRing.Set(keyring.Item{ _ = simklRingSet("SimklTokenType", []byte(simklJwt.TokenType))
Key: "SimklTokenType", _ = simklRingSet("SimklAccessToken", []byte(simklJwt.AccessToken))
Data: []byte(simklJwt.TokenType), _ = simklRingSet("SimklScope", []byte(simklJwt.Scope))
}) a.app.Dialog.Info().
_ = simklRing.Set(keyring.Item{ SetTitle("Simkl Authorization").
Key: "SimklAccessToken", SetMessage("It is now safe to close your browser tab").
Data: []byte(simklJwt.AccessToken), Show()
})
_ = 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)
}
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
simklCancel() simklCancel()
err = srv.Shutdown(context.Background()) if err := srv.Shutdown(context.Background()); err != nil {
if err != nil {
log.Println("server.Shutdown:", err) log.Println("server.Shutdown:", err)
} }
} else { } else {
@@ -218,12 +211,12 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
func (a *App) LogoutSimkl() string { func (a *App) LogoutSimkl() string {
if (SimklJWT{} != simklJwt) { if (SimklJWT{} != simklJwt) {
tokenTypeErr := simklRing.Remove("SimklTokenType") tokenTypeErr := keyring.Delete(simklKeyringService, "SimklTokenType")
accessTokenErr := simklRing.Remove("SimklAccessToken") accessTokenErr := keyring.Delete(simklKeyringService, "SimklAccessToken")
scopeErr := simklRing.Remove("SimklScope") scopeErr := keyring.Delete(simklKeyringService, "SimklScope")
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil { 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{} 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 package main
import ( import (
"context"
_ "embed" _ "embed"
"log"
"strings" "strings"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/wailsapp/wails/v3/pkg/application"
) )
//go:embed wails.json //go:embed wails.json
var wailsJSON string var wailsJSON string
var wailsContext *context.Context
// App struct // App struct
type App struct { type App struct {
ctx context.Context app *application.App
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { func NewApp(app *application.App) *App {
return &App{} return &App{app: app}
} }
// startup is called when the app starts. The context is saved // appVersion reads the product version from wails.json.
// so we can call the runtime methods func appVersion() string {
func (a *App) startup(ctx context.Context) { return gjson.Get(wailsJSON, "info.productVersion").String()
version := gjson.Get(wailsJSON, "info.productVersion")
wailsContext = &ctx
runtime.WindowSetTitle(ctx, "AniTrack "+version.String())
//runtime.WindowMaximise(ctx)
} }
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { // appTitle is the window title: "AniTrack <version>".
var secondInstanceArgs = secondInstanceData.Args func appTitle() string {
return "AniTrack " + appVersion()
}
println("user opened second instance", strings.Join(secondInstanceData.Args, ",")) func (a *App) onSecondInstanceLaunch(data application.SecondInstanceData) {
println("user opened second from", secondInstanceData.WorkingDirectory) println("user opened second instance", strings.Join(data.Args, ","))
runtime.WindowUnminimise(*wailsContext) println("user opened second from", data.WorkingDir)
runtime.Show(*wailsContext) if w := a.app.Window.Current(); w != nil {
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs) w.Restore()
w.Focus()
}
go a.app.Event.Emit("launchArgs", data.Args)
} }
func (a *App) ShowVersion() { func (a *App) ShowVersion() {
version := gjson.Get(wailsJSON, "info.productVersion") a.app.Dialog.Info().
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{ SetTitle("Version").
Title: "Version", SetMessage("AniTrack Version: " + appVersion()).
Message: "AniTrack Version: " + version.String(), Show()
})
if err != nil {
log.Println(err)
}
} }
+398
View File
@@ -0,0 +1,398 @@
version: '3'
tasks:
go:mod:tidy:
summary: Runs `go mod tidy`
internal: true
# Universal/multi-arch builds invoke this concurrently from parallel deps;
# two `go mod tidy` processes racing on go.mod can corrupt it (#4637).
run: once
cmds:
- go mod tidy
install:frontend:deps:
summary: Install frontend dependencies
run: once
cmds:
- task: install:frontend:deps:{{.PACKAGE_MANAGER}}
install:frontend:deps:npm:
dir: frontend
sources:
- package.json
- package-lock.json
generates:
- node_modules
preconditions:
- sh: npm version
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
cmds:
- npm install
install:frontend:deps:bun:
dir: frontend
sources:
- package.json
- bun.lock
- bun.lockb
generates:
- node_modules
preconditions:
- sh: bun --version
msg: "bun not found"
cmds:
- bun install
install:frontend:deps:pnpm:
dir: frontend
sources:
- package.json
- pnpm-lock.yaml
generates:
- node_modules
preconditions:
- sh: pnpm --version
msg: "pnpm not found"
cmds:
- pnpm install
install:frontend:deps:yarn:
dir: frontend
sources:
- package.json
- yarn.lock
status:
- test -d node_modules || test -f .pnp.cjs
preconditions:
- sh: yarn --version
msg: "yarn not found"
cmds:
- yarn install
build:frontend:
label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}})
summary: Build the frontend project
# darwin:build:universal runs its per-arch builds as parallel deps, each of
# which depends on this task. Without run:once the two executions race:
# one regenerates frontend/bindings (-clean deletes it first) while the
# other's bundler is reading it, failing intermittently with
# 'Could not resolve "./bindings/<pkg>"' (#4637).
run: once
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:
- task: install:frontend:deps
- task: generate:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
OBFUSCATED:
ref: .OBFUSCATED
cmds:
- task: frontend:run
vars:
SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
env:
PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
frontend:run:
summary: Run a frontend script with selected runner
cmds:
- task: frontend:run:{{.PACKAGE_MANAGER}}
vars:
SCRIPT: "{{.SCRIPT}}"
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:npm:
dir: frontend
cmds:
- npm run {{.SCRIPT}} -q
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:yarn:
dir: frontend
cmds:
- yarn {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:pnpm:
dir: frontend
cmds:
- pnpm run {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:run:bun:
dir: frontend
cmds:
- bun run {{.SCRIPT}}
vars:
SCRIPT: "{{.SCRIPT}}"
frontend:vendor:puppertino:
summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
sources:
- frontend/public/puppertino/puppertino.css
generates:
- frontend/public/puppertino/puppertino.css
cmds:
- |
set -euo pipefail
mkdir -p frontend/public/puppertino
# If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
if [ ! -f frontend/public/puppertino/puppertino.css ]; then
echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
else
echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
fi
else
echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
fi
# Ensure index.html includes Puppertino CSS and button classes
INDEX_HTML=frontend/index.html
if [ -f "$INDEX_HTML" ]; then
if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
# Insert Puppertino link tag after style.css link
awk '
/href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
fi
# Replace default .btn with Puppertino primary button classes if present
sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
fi
generate:bindings:
summary: Generates bindings for the frontend
run: once
deps:
- task: go:mod:tidy
sources:
- "**/*.[jt]s"
- exclude: frontend/**/*
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
run: once
dir: build
sources:
- "appicon.png"
- "appicon.icon"
generates:
- "darwin/icons.icns"
- "windows/icon.ico"
cmds:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- task: frontend:dev:{{.PACKAGE_MANAGER}}
frontend:dev:npm:
dir: frontend
cmds:
- npm run dev -- --port {{.VITE_PORT}} --strictPort
frontend:dev:yarn:
dir: frontend
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
frontend:dev:pnpm:
dir: frontend
cmds:
- pnpm dev --port {{.VITE_PORT}} --strictPort
frontend:dev:bun:
dir: frontend
cmds:
- bun run dev --port {{.VITE_PORT}} --strictPort
update:build-assets:
summary: Updates the build assets
dir: build
cmds:
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
build:server:
summary: Builds the application in server mode (no GUI, HTTP server only)
desc: |
Builds a production server binary by default: -tags server,production,
-trimpath and a stripped binary, mirroring the desktop `build` task.
Server mode runs as a pure HTTP server without native GUI dependencies.
Usage: task build:server [DEV=true] [OBFUSCATED=true] [EXTRA_TAGS=tag1,tag2]
DEV=true development server (-tags server, no strip, inlining kept)
OBFUSCATED=true obfuscated build via garble (requires garble installed)
EXTRA_TAGS additional comma-separated build tags
deps:
- task: build:frontend
vars:
DEV:
ref: .DEV
BUILD_FLAGS:
ref: .BUILD_FLAGS
OBFUSCATED:
ref: .OBFUSCATED
preconditions:
- sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
cmds:
- '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
vars:
BUILD_FLAGS: '-tags server{{if eq .DEV "true"}}{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -buildvcs=false -gcflags=all="-l"{{else}},production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
run:server:
summary: Builds and runs a development server (DEV=true)
deps:
- task: build:server
vars:
DEV: "true"
cmds:
- '"./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
build:docker:
summary: Builds a Docker image for server mode deployment
desc: |
Creates a minimal Docker image containing the production server binary.
Defaults to a pure-Go static binary on a distroless/static base. The
production frontend is built first so the embedded assets are current.
Usage: task build:docker [TAG=myapp:latest] [CGO_ENABLED=1] [GO_IMAGE=...] [RUNTIME_IMAGE=...]
For CGO apps, set CGO_ENABLED=1 with libc-compatible builder/runtime images, e.g.:
task build:docker CGO_ENABLED=1 GO_IMAGE=golang:bookworm RUNTIME_IMAGE=gcr.io/distroless/base-debian12
deps:
# Build the production frontend so frontend/dist (embedded by the Go build
# inside the image) is present and current in the Docker build context.
# Pass the server,production tags so binding generation analyses the same
# build the Docker image compiles, not the default-tag build.
- task: build:frontend
vars:
BUILD_FLAGS: "-tags server,production"
cmds:
- >-
docker build
--build-arg CGO_ENABLED={{.CGO_ENABLED | default "0"}}
--build-arg GO_IMAGE={{.GO_IMAGE | default "golang:alpine"}}
--build-arg RUNTIME_IMAGE={{.RUNTIME_IMAGE | default "gcr.io/distroless/static-debian12"}}
-t {{.TAG | default (printf "%s:latest" .APP_NAME)}}
-f build/docker/Dockerfile.server .
vars:
TAG: "{{.TAG}}"
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
- sh: test -f build/docker/Dockerfile.server
msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
run:docker:
summary: Builds and runs the Docker image
desc: |
Builds the Docker image and runs it, exposing port 8080.
Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
Note: The internal container port is always 8080. The PORT variable
only changes the host port mapping. Ensure your app uses port 8080
or modify the Dockerfile to match your ServerOptions.Port setting.
deps:
- task: build:docker
vars:
TAG:
ref: .TAG
cmds:
- docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
vars:
TAG: "{{.TAG}}"
PORT: "{{.PORT}}"
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
desc: |
Builds the Docker image needed for cross-compiling to any platform.
Run this once to enable cross-platform builds from any OS.
cmds:
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
ios:device:list:
summary: Lists connected iOS devices (UDIDs)
cmds:
- xcrun xcdevice list
ios:run:device:
summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
vars:
PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
SCHEME: '{{.SCHEME}}' # e.g., ios.dev
CONFIG: '{{.CONFIG | default "Debug"}}'
DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
UDID: '{{.UDID}}' # from `task ios:device:list`
BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
preconditions:
- sh: xcrun -f xcodebuild
msg: "xcodebuild not found. Please install Xcode."
- sh: xcrun -f devicectl
msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
- sh: test -n '{{.PROJECT}}'
msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
- sh: test -n '{{.SCHEME}}'
msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
- sh: test -n '{{.UDID}}'
msg: "Set UDID to your device UDID (see: task ios:device:list)."
- sh: test -n '{{.BUNDLE_ID}}'
msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
cmds:
- |
set -euo pipefail
echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
XCB_ARGS=(
-project "{{.PROJECT}}"
-scheme "{{.SCHEME}}"
-configuration "{{.CONFIG}}"
-destination "id={{.UDID}}"
-derivedDataPath "{{.DERIVED}}"
-allowProvisioningUpdates
-allowProvisioningDeviceRegistration
)
# Optionally inject signing identifiers if provided
if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
# If xcpretty isn't installed, run without it
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
xcodebuild "${XCB_ARGS[@]}" build
fi
# Find built .app
APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
exit 1
fi
echo "Installing: $APP_PATH"
xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
echo "Launching: {{.BUNDLE_ID}}"
xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
+42
View File
@@ -0,0 +1,42 @@
# Wails v3 project configuration.
# NOTE: `make release` bumps `info.version` below alongside wails.json.
# Never touch the top-level `version: '3'` — that is the config file schema,
# not the app version.
version: '3'
info:
companyName: "John O'Keefe"
productName: "AniTrack"
productIdentifier: "com.linuxhg.anitrack"
description: "Track anime watchlists across AniList, MyAnimeList, and Simkl"
copyright: "John O'Keefe"
version: "1.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>
+37
View File
@@ -0,0 +1,37 @@
# git-cliff configuration — generates the body of each Gitea Release from
# Conventional Commits accumulated since the previous tag. Invoked locally by
# `make release VERSION=...`; the annotated tag message becomes the Release
# body in .gitea/workflows/release.yml. Tags are plain versions (1.6.7).
# Docs: https://git-cliff.org/docs/configuration
[changelog]
header = ""
body = """
{% for group, commits in commits | group_by(attribute="group") %}\
### {{ group | upper_first }}
{% for commit in commits %}\
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
{% endfor %}\
{% endfor %}\
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = false
require_conventional = false
split_commits = false
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Tests" },
{ message = "^chore|^ci", group = "Miscellaneous Tasks" },
{ message = ".*", group = "Other" },
]
filter_commits = false
tag_pattern = "[0-9].*"
sort_commits = "oldest"
+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", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build:dev": "vite build --minify false --mode development",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json"
@@ -27,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@wailsio/runtime": "3.0.0-beta.20",
"flowbite": "^2.5.1", "flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16", "flowbite-svelte": "^0.46.16",
"moment": "^2.30.1" "moment": "^2.30.1"
+4 -8
View File
@@ -20,11 +20,7 @@
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte"; import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte"; import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import { import {App} from "../bindings/AniTrack";
CheckIfAniListLoggedIn,
GetMyAnimeList,
SimklGetUserWatchlist,
} from "../wailsjs/go/main/App";
import { loc } from "svelte-spa-router"; import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte"; import ErrorModal from "./helperComponents/ErrorModal.svelte";
@@ -49,10 +45,10 @@
await CheckIfAniListLoggedInAndLoadWatchList(); await CheckIfAniListLoggedInAndLoadWatchList();
} }
if ($malLoggedIn && $malPrimary) { if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w)); await App.GetMyAnimeList(1000).then((w) => malWatchList.set(w));
} }
if ($simklLoggedIn && $simklPrimary) { if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w)); await App.SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
} }
watchlistNeedsRefresh.set(false); watchlistNeedsRefresh.set(false);
@@ -67,7 +63,7 @@
"/": Home, "/": Home,
"/anime/:id": wrap({ "/anime/:id": wrap({
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"), asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
conditions: [async () => await CheckIfAniListLoggedIn()], conditions: [async () => await App.CheckIfAniListLoggedIn()],
loadingComponent: Spinner, loadingComponent: Spinner,
}), }),
// '*': "Not Found" // '*': "Not Found"
+89 -34
View File
@@ -4,6 +4,7 @@
aniListLoggedIn, aniListLoggedIn,
malAnime, malAnime,
malLoggedIn, malLoggedIn,
setApiError,
simklAnime, simklAnime,
simklLoggedIn, simklLoggedIn,
watchlistNeedsRefresh, watchlistNeedsRefresh,
@@ -27,16 +28,7 @@
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes"; import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes"; import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList"; import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import { import {App} from "../../bindings/AniTrack";
AniListDeleteEntry,
AniListUpdateEntry,
DeleteMyAnimeListEntry,
MyAnimeListUpdate,
SimklSyncEpisodes,
SimklSyncRating,
SimklSyncRemove,
SimklSyncStatus,
} from "../../wailsjs/go/main/App";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte"; import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte"; import Datepicker from "./Datepicker.svelte";
@@ -206,11 +198,16 @@
startedAt: convertDateToAniList(startedAtDate), startedAt: convertDateToAniList(startedAtDate),
completedAt: convertDateToAniList(completedAtDate), completedAt: convertDateToAniList(completedAtDate),
}; };
await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => { await App.AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => {
value.data.MediaList.media.tags = if (!value.data?.MediaList || value.data.MediaList.mediaId === 0) {
currentAniListAnime.data.MediaList.media.tags; setApiError(
value.data.MediaList.media.genres = "anilist",
currentAniListAnime.data.MediaList.media.genres; "AniList update failed: no saved entry was returned",
undefined,
true,
);
return;
}
aniListAnime.update((newValue) => { aniListAnime.update((newValue) => {
newValue = value; newValue = value;
return newValue; return newValue;
@@ -233,7 +230,18 @@
}); });
}); });
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("Error submitting AniList changes:", error);
setApiError(
"anilist",
`Failed to sync AniList: ${errorMsg}`,
undefined,
true,
);
}
try {
if (malLoggedIn && currentMalAnime.id !== 0) { if (malLoggedIn && currentMalAnime.id !== 0) {
let body: MALUploadStatus = { let body: MALUploadStatus = {
status: submitData.status.mal, status: submitData.status.mal,
@@ -244,7 +252,7 @@
comments: submitData.notes, comments: submitData.notes,
}; };
await MyAnimeListUpdate(currentMalAnime, body).then( await App.MyAnimeListUpdate(currentMalAnime, body).then(
(malAnimeReturn: MalListStatus) => { (malAnimeReturn: MalListStatus) => {
malAnime.update((value) => { malAnime.update((value) => {
value.my_list_status.status = malAnimeReturn.status; value.my_list_status.status = malAnimeReturn.status;
@@ -286,10 +294,21 @@
}, },
); );
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("Error submitting MyAnimeList changes:", error);
setApiError(
"mal",
`Failed to sync MyAnimeList: ${errorMsg}`,
undefined,
true,
);
}
try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
if (currentSimklAnime.watched_episodes_count !== submitData.episodes) { if (currentSimklAnime.watched_episodes_count !== submitData.episodes) {
await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then( await App.SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
(value: SimklAnime) => { (value: SimklAnime) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
@@ -312,7 +331,7 @@
} }
if (currentSimklAnime.user_rating !== submitData.rating) { if (currentSimklAnime.user_rating !== submitData.rating) {
await SimklSyncRating(currentSimklAnime, submitData.rating).then( await App.SimklSyncRating(currentSimklAnime, submitData.rating).then(
(value) => { (value) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
@@ -335,7 +354,7 @@
} }
if (currentSimklAnime.status !== submitData.status.simkl) { if (currentSimklAnime.status !== submitData.status.simkl) {
await SimklSyncStatus( await App.SimklSyncStatus(
currentSimklAnime, currentSimklAnime,
submitData.status.simkl, submitData.status.simkl,
).then((value) => { ).then((value) => {
@@ -359,13 +378,19 @@
} }
} }
} catch (error) { } catch (error) {
console.error("Error submitting changes:", error); const errorMsg = error instanceof Error ? error.message : String(error);
} finally { console.error("Error submitting Simkl changes:", error);
submitting.set(false); setApiError(
submitSuccess.set(true); "simkl",
watchlistNeedsRefresh.set(true); `Failed to sync Simkl: ${errorMsg}`,
setTimeout(() => submitSuccess.set(false), 2000); undefined,
true,
);
} }
submitting.set(false);
submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000);
}; };
const deleteEntries = async () => { const deleteEntries = async () => {
@@ -375,7 +400,7 @@
isAniListLoggedIn && isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
) { ) {
await AniListDeleteEntry(currentAniListAnime.data.MediaList.id); await App.AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `a-${currentAniListAnime.data.MediaList.mediaId}`, id: `a-${currentAniListAnime.data.MediaList.mediaId}`,
title, title,
@@ -389,8 +414,20 @@
notes: "", notes: "",
}); });
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("Error deleting AniList entry:", error);
setApiError(
"anilist",
`Failed to delete AniList entry: ${errorMsg}`,
undefined,
true,
);
}
try {
if (malLoggedIn && currentMalAnime.id !== 0) { if (malLoggedIn && currentMalAnime.id !== 0) {
await DeleteMyAnimeListEntry(currentMalAnime.id); await App.DeleteMyAnimeListEntry(currentMalAnime.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`, id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title, title: currentMalAnime.title,
@@ -404,8 +441,20 @@
notes: "", notes: "",
}); });
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("Error deleting MyAnimeList entry:", error);
setApiError(
"mal",
`Failed to delete MyAnimeList entry: ${errorMsg}`,
undefined,
true,
);
}
try {
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
await SimklSyncRemove(currentSimklAnime); await App.SimklSyncRemove(currentSimklAnime);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${currentSimklAnime.show.ids.simkl}`, id: `s-${currentSimklAnime.show.ids.simkl}`,
title: currentSimklAnime.show.title, title: currentSimklAnime.show.title,
@@ -420,13 +469,19 @@
}); });
} }
} catch (error) { } catch (error) {
console.error("Error deleting entries:", error); const errorMsg = error instanceof Error ? error.message : String(error);
} finally { console.error("Error deleting Simkl entry:", error);
submitting.set(false); setApiError(
submitSuccess.set(true); "simkl",
watchlistNeedsRefresh.set(true); `Failed to delete Simkl entry: ${errorMsg}`,
setTimeout(() => submitSuccess.set(false), 2000); undefined,
true,
);
} }
submitting.set(false);
submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000);
}; };
let max = 999; let max = 999;
@@ -16,10 +16,10 @@
logoutOfSimkl, logoutOfSimkl,
serviceLoggingIn, serviceLoggingIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import * as runtime from "../../wailsjs/runtime"; import {Application} from "@wailsio/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes"; import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes"; import type { SimklUser } from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
let currentAniListUser: AniListUser; let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser; let currentMALUser: MyAnimeListUser;
@@ -189,14 +189,14 @@
<button <button
on:click={() => { on:click={() => {
dropdownUser(); dropdownUser();
ShowVersion(); App.ShowVersion();
}} }}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
> >
Version Version
</button> </button>
<button <button
on:click={() => runtime.Quit()} on:click={() => Application.Quit()}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
> >
Exit Application Exit Application
@@ -8,7 +8,7 @@
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType"; import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
let aniListWatchListLoaded: AniListCurrentUserWatchList; let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number; let page: number;
@@ -23,7 +23,7 @@
const perPageOptions = [10, 20, 50]; const perPageOptions = [10, 20, 50];
function ChangeWatchListPage(newPage: number) { function ChangeWatchListPage(newPage: number) {
GetAniListUserWatchingList(newPage, perPage, sort).then((result) => { App.GetAniListUserWatchingList(newPage, perPage, sort).then((result) => {
watchListPage.set(newPage); watchListPage.set(newPage);
aniListWatchlist.set(result); aniListWatchlist.set(result);
aniListLoggedIn.set(true); aniListLoggedIn.set(true);
@@ -43,7 +43,7 @@
function changeCountPerPage( function changeCountPerPage(
e: Event & { currentTarget: HTMLSelectElement }, e: Event & { currentTarget: HTMLSelectElement },
): void { ): void {
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then( App.GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
(result) => { (result) => {
animePerPage.set(Number(e.currentTarget.value)); animePerPage.set(Number(e.currentTarget.value));
watchListPage.set(1); watchListPage.set(1);
@@ -6,7 +6,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="button" type="button"
class="py-2 px-4 mt-4 mr-4 bg-gray-700 rounded-lg" class="py-2 px-4 mt-4 mr-4 text-nowrap bg-gray-700 rounded-lg"
on:click={async () => { on:click={async () => {
loading.set(true); loading.set(true);
await CheckIfAniListLoggedInAndLoadWatchList(); await CheckIfAniListLoggedInAndLoadWatchList();
+148 -32
View File
@@ -1,27 +1,132 @@
<script lang="ts"> <script lang="ts">
import {AniListSearch} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import type {AniSearchList} from "../anilist/types/AniListTypes"; import type {AniSearchList} from "../anilist/types/AniListTypes";
import {push} from "svelte-spa-router"; import {push} from "svelte-spa-router";
let aniSearch = "" let aniSearch = ""
let aniListSearch: AniSearchList let aniListSearch: AniSearchList | null = null
let aniListSearchActive = false let dropdownOpen = false
let isSearching = false
let showSlowNotice = false
let searchError: string | null = null
let hasSearched = false
let searchRequestId = 0
function runAniListSearch(): void { const SLOW_NOTICE_MS = 8000
AniListSearch(aniSearch).then(result => { const SEARCH_TIMEOUT_MS = 30000
aniListSearch = result
aniListSearchActive = true function openDropdown(): void {
}) dropdownOpen = true
} }
function searchDropdown(): void { function closeDropdown(): void {
let dropdown = document.querySelector("#aniListSearchDropdown") dropdownOpen = false
dropdown.classList.toggle("hidden") }
function displayTitle(media: { title?: { english?: string | null; romaji?: string | null; native?: string | null } }): string {
const english = media?.title?.english
if (english !== undefined && english !== null && english !== "") {
return english
}
const romaji = media?.title?.romaji
if (romaji !== undefined && romaji !== null && romaji !== "") {
return romaji
}
return media?.title?.native ?? "Unknown title"
}
function resultCount(): number {
const media = aniListSearch?.data?.Page?.media
return Array.isArray(media) ? media.length : 0
}
async function runAniListSearch(): Promise<void> {
const term = aniSearch.trim()
openDropdown()
if (term.length === 0) {
searchRequestId += 1
aniListSearch = null
searchError = null
hasSearched = false
isSearching = false
showSlowNotice = false
return
}
if (isSearching) {
return
}
if (typeof navigator !== "undefined" && !navigator.onLine) {
aniListSearch = null
hasSearched = true
searchError = "You appear to be offline. Check your connection and try again."
return
}
isSearching = true
showSlowNotice = false
searchError = null
hasSearched = true
const myRequest = searchRequestId + 1
searchRequestId = myRequest
let slowTimer: ReturnType<typeof setTimeout> | null = null
let timeoutTimer: ReturnType<typeof setTimeout> | null = null
try {
slowTimer = setTimeout(() => {
if (searchRequestId === myRequest && isSearching) {
showSlowNotice = true
}
}, SLOW_NOTICE_MS)
const timeout = new Promise<never>((_, reject) => {
timeoutTimer = setTimeout(() => {
reject(new Error("AniList is taking too long to respond. Please try again."))
}, SEARCH_TIMEOUT_MS)
})
const result = await Promise.race([App.AniListSearch(term), timeout])
if (searchRequestId !== myRequest) {
return
}
aniListSearch = result as AniSearchList
if (!Array.isArray(aniListSearch?.data?.Page?.media)) {
aniListSearch = null
}
} catch (e) {
if (searchRequestId !== myRequest) {
return
}
aniListSearch = null
searchError = e instanceof Error ? e.message : String(e)
} finally {
if (slowTimer !== null) {
clearTimeout(slowTimer)
}
if (timeoutTimer !== null) {
clearTimeout(timeoutTimer)
}
if (searchRequestId === myRequest) {
isSearching = false
showSlowNotice = false
}
}
}
function goToAnime(id: number): void {
closeDropdown()
push(`#/anime/${id}`)
}
function handleWindowClick(e: MouseEvent): void {
if (!dropdownOpen) {
return
}
const container = document.querySelector("#searchDropdown")
if (container && e.target instanceof Node && !container.contains(e.target)) {
closeDropdown()
}
} }
</script> </script>
<svelte:window on:click={handleWindowClick} />
<div id="searchDropdown" class="relative w-64 md:w-48"> <div id="searchDropdown" class="relative w-64 md:w-48">
<div class="flex"> <div class="flex">
@@ -33,16 +138,20 @@
placeholder="Search for Anime" placeholder="Search for Anime"
on:keypress={(e) => { on:keypress={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
searchDropdown() runAniListSearch()
if(aniSearch.length > 0) runAniListSearch() }
}}
on:keydown={(e) => {
if (e.key === "Escape") {
closeDropdown()
} }
}} }}
required/> required/>
<button id="aniListSearchButton" <button id="aniListSearchButton"
class="absolute top-0 end-0 h-full p-2.5 text-sm font-medium rounded-e-lg border focus:ring-4 focus:outline-none bg-blue-600 hover:bg-blue-700 focus:ring-blue-800" class="absolute top-0 end-0 h-full p-2.5 text-sm font-medium rounded-e-lg border focus:ring-4 focus:outline-none bg-blue-600 hover:bg-blue-700 focus:ring-blue-800 disabled:opacity-50"
disabled={isSearching}
on:click={() => { on:click={() => {
searchDropdown() runAniListSearch()
if(aniSearch.length > 0) runAniListSearch()
}}> }}>
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 20 20"> viewBox="0 0 20 20">
@@ -54,31 +163,38 @@
</div> </div>
</div> </div>
<div id="aniListSearchDropdown" class="z-10 absolute left-0 hidden bg-white rounded-lg shadow w-60 2xl:w-80 dark:bg-gray-700"> <div id="aniListSearchDropdown" class:hidden={!dropdownOpen} class="z-10 absolute left-0 bg-white rounded-lg shadow w-60 2xl:w-80 dark:bg-gray-700">
{#if aniListSearchActive} {#if isSearching}
<div class="m-4 text-gray-700 dark:text-gray-200">{showSlowNotice ? "AniList is slow today, still trying..." : "Searching AniList..."}</div>
{:else if searchError}
<div class="m-4">
<p class="text-red-600 dark:text-red-400 font-medium">Search failed</p>
<p class="mt-1 text-sm text-gray-700 dark:text-gray-200">{searchError}</p>
<button class="mt-3 text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
on:click={() => runAniListSearch()}>
Retry search
</button>
</div>
{:else if !hasSearched && aniSearch.trim().length === 0}
<div class="m-4 text-gray-700 dark:text-gray-200">Please enter a search term...</div>
{:else if resultCount() > 0 && aniListSearch}
<ul class="h-56 w-full py-2 overflow-y-auto text-gray-700 dark:text-gray-200" <ul class="h-56 w-full py-2 overflow-y-auto text-gray-700 dark:text-gray-200"
aria-labelledby="aniListSearchButton"> aria-labelledby="aniListSearchButton">
{#each aniListSearch.data.Page.media as media} {#each aniListSearch.data.Page.media as media (media.id)}
<li class="w-full"> <li class="w-full">
<div class="flex w-full items-start p-1 hover:bg-gray-600 hover:text-white rounded-lg"> <div class="flex w-full items-start p-1 hover:bg-gray-600 hover:text-white rounded-lg">
<button on:click={() => { <button on:click={() => goToAnime(media.id)}
searchDropdown()
push(`#/anime/${media.id}`)
}}
> >
<img class="rounded-bl-lg rounded-tl-lg max-w-24 max-h-24" src={media.coverImage.large} <img class="rounded-bl-lg rounded-tl-lg max-w-24 max-h-24" src={media?.coverImage?.large}
alt="{media.title.english === '' || media.title.english === null ? media.title.romaji : media.title.english} Cover"> alt="{displayTitle(media)} Cover">
</button> </button>
<button class="rounded-bl-lg rounded-tl-lg w-full h-24" on:click={() => { <button class="rounded-bl-lg rounded-tl-lg w-full h-24" on:click={() => goToAnime(media.id)} >{displayTitle(media)}</button>
searchDropdown()
push(`#/anime/${media.id}`)
}} >{media.title.english === '' || media.title.english === null ? media.title.romaji : media.title.english }</button>
</div> </div>
</li> </li>
{/each} {/each}
</ul> </ul>
{:else if aniSearch.length === 0} {:else if hasSearched}
<div class="m-4">Please enter a search term...</div> <div class="m-4 text-gray-700 dark:text-gray-200">No results found for "{aniSearch.trim()}". Try a different title.</div>
{/if} {/if}
</div> </div>
</div> </div>
+3 -3
View File
@@ -6,7 +6,7 @@
aniListWatchlist, aniListWatchlist,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { MediaListSort } from "../anilist/types/AniListTypes"; import { MediaListSort } from "../anilist/types/AniListTypes";
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
const sortTypes = [ const sortTypes = [
{ value: MediaListSort.MediaId, name: "Media Id Asc" }, { value: MediaListSort.MediaId, name: "Media Id Asc" },
@@ -55,7 +55,7 @@
console.log(sort); console.log(sort);
async function changeWatchListSort() { async function changeWatchListSort() {
const result = await GetAniListUserWatchingList( const result = await App.GetAniListUserWatchingList(
$watchListPage, $watchListPage,
$animePerPage, $animePerPage,
$aniListSort, $aniListSort,
@@ -66,7 +66,7 @@
<select <select
id="sort" id="sort"
class="border rounded-lg block p-1.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500" class="border rounded-lg block max-h-10 mt-4 mx-2 p-1.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
bind:value={$aniListSort} bind:value={$aniListSort}
on:change={() => changeWatchListSort()} on:change={() => changeWatchListSort()}
> >
@@ -11,6 +11,7 @@
import loader from "../helperFunctions/loader"; import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Sort from "../helperComponents/Sort.svelte"; import Sort from "../helperComponents/Sort.svelte";
import RefreshWatchListButton from "./RefreshWatchListButton.svelte";
let isAniListLoggedIn: boolean; let isAniListLoggedIn: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList; let aniListWatchListLoaded: AniListCurrentUserWatchList;
@@ -26,7 +27,10 @@
> >
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h1 class="text-left text-xl font-bold">Your AniList WatchList</h1> <h1 class="text-left text-xl font-bold">Your AniList WatchList</h1>
<Sort /> <div class="flex">
<Sort />
<RefreshWatchListButton />
</div>
</div> </div>
<div <div
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { BrowserOpenURL } from "../../wailsjs/runtime"; import {Browser} from "@wailsio/runtime";
export let id: string; export let id: string;
export let url = ""; export let url = "";
@@ -25,7 +25,7 @@
<button <button
type="button" type="button"
class="underline underline-offset-2 px-4 py-1" class="underline underline-offset-2 px-4 py-1"
on:click={() => BrowserOpenURL(url)}>{newId}</button on:click={() => Browser.OpenURL(url)}>{newId}</button
> >
{:else} {:else}
{id} {id}
@@ -1,9 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {App} from "../../bindings/AniTrack";
CheckIfAniListLoggedIn,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
} from "../../wailsjs/go/main/App";
import { import {
aniListUser, aniListUser,
watchListPage, watchListPage,
@@ -29,7 +25,7 @@
export const LoadAniListUser = async () => { export const LoadAniListUser = async () => {
try { try {
await GetAniListLoggedInUser().then((user) => { await App.GetAniListLoggedInUser().then((user) => {
aniListUser.set(user); aniListUser.set(user);
}); });
} catch (err) { } catch (err) {
@@ -46,7 +42,7 @@
export const LoadAniListWatchList = async () => { export const LoadAniListWatchList = async () => {
try { try {
const watchList = await GetAniListUserWatchingList(page, perPage, sort); const watchList = await App.GetAniListUserWatchingList(page, perPage, sort);
aniListWatchlist.set(watchList); aniListWatchlist.set(watchList);
clearApiError(); clearApiError();
} catch (err) { } catch (err) {
@@ -63,7 +59,7 @@
export const CheckIfAniListLoggedInAndLoadWatchList = async () => { export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
serviceLoggingIn.update((s) => [...s, "anilist"]); serviceLoggingIn.update((s) => [...s, "anilist"]);
try { try {
const loggedIn = await CheckIfAniListLoggedIn(); const loggedIn = await App.CheckIfAniListLoggedIn();
if (loggedIn) { if (loggedIn) {
await LoadAniListUser(); await LoadAniListUser();
if (isAniListPrimary) await LoadAniListWatchList(); if (isAniListPrimary) await LoadAniListWatchList();
@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import {CheckIfMyAnimeListLoggedIn, GetMyAnimeList, GetMyAnimeListLoggedInUser} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte" import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte"
import type { MyAnimeListUser } from "../mal/types/MALTypes"; import type { MyAnimeListUser } from "../mal/types/MALTypes";
@@ -8,9 +8,9 @@
export const CheckIfMALLoggedInAndSetUser = async () => { export const CheckIfMALLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "mal"]) serviceLoggingIn.update((s) => [...s, "mal"])
await CheckIfMyAnimeListLoggedIn().then(loggedIn => { await App.CheckIfMyAnimeListLoggedIn().then(loggedIn => {
if (loggedIn) { if (loggedIn) {
GetMyAnimeListLoggedInUser().then(user => { App.GetMyAnimeListLoggedInUser().then(user => {
if (!user.name) { if (!user.name) {
malUser.set({} as MyAnimeListUser) malUser.set({} as MyAnimeListUser)
malLoggedIn.set(false) malLoggedIn.set(false)
@@ -18,7 +18,7 @@
} }
malUser.set(user) malUser.set(user)
if (isMalPrimary) { if (isMalPrimary) {
GetMyAnimeList(1000).then(watchList => { App.GetMyAnimeList(1000).then(watchList => {
malWatchList.set(watchList) malWatchList.set(watchList)
malLoggedIn.set(loggedIn) malLoggedIn.set(loggedIn)
}) })
@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import {CheckIfSimklLoggedIn, GetSimklLoggedInUser, SimklGetUserWatchlist} from "../../wailsjs/go/main/App"; import {App} from "../../bindings/AniTrack";
import { simklLoggedIn, simklUser, simklPrimary, simklWatchList, serviceLoggingIn } from "./GlobalVariablesAndHelperFunctions.svelte"; import { simklLoggedIn, simklUser, simklPrimary, simklWatchList, serviceLoggingIn } from "./GlobalVariablesAndHelperFunctions.svelte";
let isSimklPrimary: boolean let isSimklPrimary: boolean
@@ -7,15 +7,15 @@
export const CheckIfSimklLoggedInAndSetUser = async () => { export const CheckIfSimklLoggedInAndSetUser = async () => {
serviceLoggingIn.update((s) => [...s, "simkl"]) serviceLoggingIn.update((s) => [...s, "simkl"])
await CheckIfSimklLoggedIn().then(loggedIn => { await App.CheckIfSimklLoggedIn().then(loggedIn => {
if (loggedIn) { if (loggedIn) {
GetSimklLoggedInUser().then(user => { App.GetSimklLoggedInUser().then(user => {
if (Object.keys(user).length === 0) { if (Object.keys(user).length === 0) {
simklLoggedIn.set(false) simklLoggedIn.set(false)
} else { } else {
simklUser.set(user) simklUser.set(user)
if (isSimklPrimary) { if (isSimklPrimary) {
SimklGetUserWatchlist().then(result => { App.SimklGetUserWatchlist().then(result => {
simklWatchList.set(result) simklWatchList.set(result)
simklLoggedIn.set(loggedIn) simklLoggedIn.set(loggedIn)
}) })
@@ -1,17 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {App} from "../../bindings/AniTrack";
GetAniListItem,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
GetMyAnimeListAnime,
GetMyAnimeListLoggedInUser,
GetSimklLoggedInUser,
LogoutAniList,
LogoutMyAnimeList,
LogoutSimkl,
SimklGetUserWatchlist,
SimklSearch,
} from "../../wailsjs/go/main/App";
import type { import type {
AniListCurrentUserWatchList, AniListCurrentUserWatchList,
AniListGetSingleAnime, AniListGetSingleAnime,
@@ -109,7 +97,7 @@
aniId: number, aniId: number,
login: boolean, login: boolean,
): Promise<""> { ): Promise<""> {
await GetAniListItem(aniId, login).then((aniListResult) => { await App.GetAniListItem(aniId, login).then((aniListResult) => {
let finalResult: AniListGetSingleAnime; let finalResult: AniListGetSingleAnime;
finalResult = aniListResult; finalResult = aniListResult;
if (login === false) { if (login === false) {
@@ -133,14 +121,14 @@
); );
}); });
if (isMalLoggedIn) { if (isMalLoggedIn) {
await GetMyAnimeListAnime( await App.GetMyAnimeListAnime(
currentAniListAnime.data.MediaList.media.idMal, currentAniListAnime.data.MediaList.media.idMal,
).then((malResult) => { ).then((malResult) => {
malAnime.set(malResult); malAnime.set(malResult);
}); });
} }
if (isSimklLoggedIn) { if (isSimklLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then( await App.SimklSearch(currentAniListAnime.data.MediaList).then(
(value: SimklAnime) => { (value: SimklAnime) => {
simklAnime.set(value); simklAnime.set(value);
}, },
@@ -168,13 +156,13 @@
export function loginToSimkl(): void { export function loginToSimkl(): void {
setServiceLoggingIn("simkl", true); setServiceLoggingIn("simkl", true);
GetSimklLoggedInUser() App.GetSimklLoggedInUser()
.then((user) => { .then((user) => {
if (Object.keys(user).length === 0) { if (Object.keys(user).length === 0) {
simklLoggedIn.set(false); simklLoggedIn.set(false);
} else { } else {
simklUser.set(user); simklUser.set(user);
SimklGetUserWatchlist().then((result) => { App.SimklGetUserWatchlist().then((result) => {
simklWatchList.set(result); simklWatchList.set(result);
simklLoggedIn.set(true); simklLoggedIn.set(true);
}); });
@@ -185,11 +173,11 @@
export function loginToAniList(): void { export function loginToAniList(): void {
setServiceLoggingIn("anilist", true); setServiceLoggingIn("anilist", true);
GetAniListLoggedInUser() App.GetAniListLoggedInUser()
.then((result) => { .then((result) => {
aniListUser.set(result); aniListUser.set(result);
if (isAniListPrimary) { if (isAniListPrimary) {
GetAniListUserWatchingList(page, perPage, sort).then((result) => { App.GetAniListUserWatchingList(page, perPage, sort).then((result) => {
aniListWatchlist.set(result); aniListWatchlist.set(result);
aniListLoggedIn.set(true); aniListLoggedIn.set(true);
}); });
@@ -202,7 +190,7 @@
export function loginToMAL(): void { export function loginToMAL(): void {
setServiceLoggingIn("mal", true); setServiceLoggingIn("mal", true);
GetMyAnimeListLoggedInUser() App.GetMyAnimeListLoggedInUser()
.then((result) => { .then((result) => {
if (!result.name) { if (!result.name) {
malUser.set({} as MyAnimeListUser); malUser.set({} as MyAnimeListUser);
@@ -216,7 +204,7 @@
} }
export function logoutOfAniList(): void { export function logoutOfAniList(): void {
LogoutAniList().then((result) => { App.LogoutAniList().then((result) => {
console.log(result); console.log(result);
if (Object.keys(aniWatchlist).length !== 0) { if (Object.keys(aniWatchlist).length !== 0) {
aniListWatchlist.set({} as AniListCurrentUserWatchList); aniListWatchlist.set({} as AniListCurrentUserWatchList);
@@ -227,7 +215,7 @@
} }
export function logoutOfMAL(): void { export function logoutOfMAL(): void {
LogoutMyAnimeList().then((result) => { App.LogoutMyAnimeList().then((result) => {
console.log(result); console.log(result);
malUser.set({} as MyAnimeListUser); malUser.set({} as MyAnimeListUser);
malLoggedIn.set(false); malLoggedIn.set(false);
@@ -235,7 +223,7 @@
} }
export function logoutOfSimkl(): void { export function logoutOfSimkl(): void {
LogoutSimkl().then((result) => { App.LogoutSimkl().then((result) => {
console.log(result); console.log(result);
simklUser.set({} as SimklUser); simklUser.set({} as SimklUser);
simklLoggedIn.set(false); simklLoggedIn.set(false);
-1
View File
@@ -46,7 +46,6 @@
</div> </div>
</div> </div>
{:else if isAniListLoggedIn && isAniListPrimary} {:else if isAniListLoggedIn && isAniListPrimary}
<RefreshWatchListButton />
<div class="container py-10"> <div class="container py-10">
<Pagination /> <Pagination />
<WatchList /> <WatchList />
+7 -1
View File
@@ -1,7 +1,13 @@
import {defineConfig} from 'vite' import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte' import {svelte} from '@sveltejs/vite-plugin-svelte'
import wails from '@wailsio/runtime/plugins/vite'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()] server: {
host: '127.0.0.1',
port: Number(process.env.WAILS_VITE_PORT) || 5173,
strictPort: true,
},
plugins: [svelte(), wails('./bindings')]
}) })
-61
View File
@@ -1,61 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function AniListBrowse(arg1:number,arg2:number,arg3:number,arg4:boolean,arg5:string,arg6:Array<string>,arg7:string,arg8:string,arg9:string,arg10:string,arg11:number,arg12:string,arg13:boolean,arg14:number,arg15:number,arg16:number,arg17:number,arg18:number,arg19:number,arg20:number,arg21:number,arg22:number,arg23:number,arg24:Array<number>,arg25:boolean,arg26:Array<string>,arg27:Array<string>,arg28:Array<string>,arg29:Array<string>,arg30:number,arg31:Array<string>):Promise<main.AniListCurrentUserWatchList>;
export function AniListDeleteEntry(arg1:number):Promise<main.DeleteAniListReturn>;
export function AniListLogin():Promise<void>;
export function AniListSearch(arg1:string):Promise<any>;
export function AniListUpdateEntry(arg1:main.AniListUpdateVariables):Promise<main.AniListGetSingleAnime>;
export function CheckIfAniListLoggedIn():Promise<boolean>;
export function CheckIfMyAnimeListLoggedIn():Promise<boolean>;
export function CheckIfSimklLoggedIn():Promise<boolean>;
export function DeleteMyAnimeListEntry(arg1:number):Promise<boolean>;
export function GetAniListItem(arg1:number,arg2:boolean):Promise<main.AniListGetSingleAnime>;
export function GetAniListLoggedInUser():Promise<main.AniListUser>;
export function GetAniListUserWatchingList(arg1:number,arg2:number,arg3:string):Promise<main.AniListCurrentUserWatchList>;
export function GetMyAnimeList(arg1:number):Promise<main.MALWatchlist>;
export function GetMyAnimeListAnime(arg1:number):Promise<main.MALAnime>;
export function GetMyAnimeListLoggedInUser():Promise<main.MyAnimeListUser>;
export function GetSimklLoggedInUser():Promise<main.SimklUser>;
export function LogoutAniList():Promise<string>;
export function LogoutMyAnimeList():Promise<string>;
export function LogoutSimkl():Promise<string>;
export function MyAnimeListLogin():Promise<void>;
export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>;
export function ShowVersion():Promise<void>;
export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>;
export function SimklLogin():Promise<void>;
export function SimklSearch(arg1:main.MediaList):Promise<main.SimklAnime>;
export function SimklSyncEpisodes(arg1:main.SimklAnime,arg2:number):Promise<main.SimklAnime>;
export function SimklSyncRating(arg1:main.SimklAnime,arg2:number):Promise<main.SimklAnime>;
export function SimklSyncRemove(arg1:main.SimklAnime):Promise<boolean>;
export function SimklSyncStatus(arg1:main.SimklAnime,arg2:string):Promise<main.SimklAnime>;
-119
View File
@@ -1,119 +0,0 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AniListBrowse(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25, arg26, arg27, arg28, arg29, arg30, arg31) {
return window['go']['main']['App']['AniListBrowse'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25, arg26, arg27, arg28, arg29, arg30, arg31);
}
export function AniListDeleteEntry(arg1) {
return window['go']['main']['App']['AniListDeleteEntry'](arg1);
}
export function AniListLogin() {
return window['go']['main']['App']['AniListLogin']();
}
export function AniListSearch(arg1) {
return window['go']['main']['App']['AniListSearch'](arg1);
}
export function AniListUpdateEntry(arg1) {
return window['go']['main']['App']['AniListUpdateEntry'](arg1);
}
export function CheckIfAniListLoggedIn() {
return window['go']['main']['App']['CheckIfAniListLoggedIn']();
}
export function CheckIfMyAnimeListLoggedIn() {
return window['go']['main']['App']['CheckIfMyAnimeListLoggedIn']();
}
export function CheckIfSimklLoggedIn() {
return window['go']['main']['App']['CheckIfSimklLoggedIn']();
}
export function DeleteMyAnimeListEntry(arg1) {
return window['go']['main']['App']['DeleteMyAnimeListEntry'](arg1);
}
export function GetAniListItem(arg1, arg2) {
return window['go']['main']['App']['GetAniListItem'](arg1, arg2);
}
export function GetAniListLoggedInUser() {
return window['go']['main']['App']['GetAniListLoggedInUser']();
}
export function GetAniListUserWatchingList(arg1, arg2, arg3) {
return window['go']['main']['App']['GetAniListUserWatchingList'](arg1, arg2, arg3);
}
export function GetMyAnimeList(arg1) {
return window['go']['main']['App']['GetMyAnimeList'](arg1);
}
export function GetMyAnimeListAnime(arg1) {
return window['go']['main']['App']['GetMyAnimeListAnime'](arg1);
}
export function GetMyAnimeListLoggedInUser() {
return window['go']['main']['App']['GetMyAnimeListLoggedInUser']();
}
export function GetSimklLoggedInUser() {
return window['go']['main']['App']['GetSimklLoggedInUser']();
}
export function LogoutAniList() {
return window['go']['main']['App']['LogoutAniList']();
}
export function LogoutMyAnimeList() {
return window['go']['main']['App']['LogoutMyAnimeList']();
}
export function LogoutSimkl() {
return window['go']['main']['App']['LogoutSimkl']();
}
export function MyAnimeListLogin() {
return window['go']['main']['App']['MyAnimeListLogin']();
}
export function MyAnimeListUpdate(arg1, arg2) {
return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2);
}
export function ShowVersion() {
return window['go']['main']['App']['ShowVersion']();
}
export function SimklGetUserWatchlist() {
return window['go']['main']['App']['SimklGetUserWatchlist']();
}
export function SimklLogin() {
return window['go']['main']['App']['SimklLogin']();
}
export function SimklSearch(arg1) {
return window['go']['main']['App']['SimklSearch'](arg1);
}
export function SimklSyncEpisodes(arg1, arg2) {
return window['go']['main']['App']['SimklSyncEpisodes'](arg1, arg2);
}
export function SimklSyncRating(arg1, arg2) {
return window['go']['main']['App']['SimklSyncRating'](arg1, arg2);
}
export function SimklSyncRemove(arg1) {
return window['go']['main']['App']['SimklSyncRemove'](arg1);
}
export function SimklSyncStatus(arg1, arg2) {
return window['go']['main']['App']['SimklSyncStatus'](arg1, arg2);
}
-828
View File
@@ -1,828 +0,0 @@
export namespace main {
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 Media {
id: number;
idMal: number;
// Go type: struct { UserPreferred string "json:\"userPreferred\""; Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" }
title: any;
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;
// Go type: struct { nodes struct { id int; Title struct { UserPreferred string "json:\"userPreferred\""; Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" } "json:\"title\"" } }
Relations: any;
// Go type: struct { Year int; Month int; Day int }
StartDate: any;
// Go type: struct { Year int; Month int; Day int }
EndDate: any;
// Go type: struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" }
nextAiringEpisode: any;
// Go type: struct { Nodes struct { Id int; AiringAt int; TimeUntilAiring int; Episode int; MediaId int } }
AiringSchedule: any;
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"], Object);
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"], Object);
this.StartDate = this.convertValues(source["StartDate"], Object);
this.EndDate = this.convertValues(source["EndDate"], Object);
this.nextAiringEpisode = this.convertValues(source["nextAiringEpisode"], Object);
this.AiringSchedule = this.convertValues(source["AiringSchedule"], Object);
this.genres = source["genres"];
this.tags = this.convertValues(source["tags"], );
this.isAdult = source["isAdult"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MediaList {
id: number;
mediaId: number;
userId: number;
status: string;
media: Media;
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
startedAt: any;
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
completedAt: any;
notes: string;
progress: number;
score: number;
repeat: number;
// Go type: struct { ID int "json:\"id\""; Name string "json:\"name\""; Avatar struct { Large string "json:\"large\""; Medium string "json:\"medium\"" } "json:\"avatar\""; Statistics struct { Anime struct { Count int "json:\"count\""; Statuses []struct { Status string "json:\"status\""; Count int "json:\"count\"" } "json:\"statuses\"" } "json:\"anime\"" } "json:\"statistics\"" }
user: any;
static createFrom(source: any = {}) {
return new MediaList(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.mediaId = source["mediaId"];
this.userId = source["userId"];
this.status = source["status"];
this.media = this.convertValues(source["media"], Media);
this.startedAt = this.convertValues(source["startedAt"], Object);
this.completedAt = this.convertValues(source["completedAt"], Object);
this.notes = source["notes"];
this.progress = source["progress"];
this.score = source["score"];
this.repeat = source["repeat"];
this.user = this.convertValues(source["user"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class MyAnimeListUser {
id: id;
name: name;
picture: picture;
gender: gender;
birthday: birthday;
location: location;
joined_at: joinedAt;
num_items_watching: numItemsWatching;
num_items_completed: numItemsCompleted;
num_items_on_hold: numItemsOnHold;
num_items_dropped: numItemsDropped;
num_items_plan_to_watch: numItemsPlanToWatch;
num_items: numItems;
num_days_watched: numDaysWatched;
num_days_watching: numDaysWatching;
num_days_completed: numDaysCompleted;
num_days_on_hold: numDaysOnHold;
num_days_dropped: numDaysDropped;
num_days: numDays;
num_episodes: numEpisodes;
num_times_rewatched: numTimesRewatched;
mean_score: meanScore;
time_zone: timeZone;
is_supporter: isSupporter;
static createFrom(source: any = {}) {
return new MyAnimeListUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.name = source["name"];
this.picture = source["picture"];
this.gender = source["gender"];
this.birthday = source["birthday"];
this.location = source["location"];
this.joined_at = source["joined_at"];
this.num_items_watching = source["num_items_watching"];
this.num_items_completed = source["num_items_completed"];
this.num_items_on_hold = source["num_items_on_hold"];
this.num_items_dropped = source["num_items_dropped"];
this.num_items_plan_to_watch = source["num_items_plan_to_watch"];
this.num_items = source["num_items"];
this.num_days_watched = source["num_days_watched"];
this.num_days_watching = source["num_days_watching"];
this.num_days_completed = source["num_days_completed"];
this.num_days_on_hold = source["num_days_on_hold"];
this.num_days_dropped = source["num_days_dropped"];
this.num_days = source["num_days"];
this.num_episodes = source["num_episodes"];
this.num_times_rewatched = source["num_times_rewatched"];
this.mean_score = source["mean_score"];
this.time_zone = source["time_zone"];
this.is_supporter = source["is_supporter"];
}
}
export class SimklAnime {
last_watched_at: last_watched_at;
status: status;
user_rating: user_rating;
last_watched: last_watched;
next_to_watch: next_to_watch;
watched_episodes_count: watched_episodes_count;
total_episodes_count: total_episodes_count;
not_aired_episodes_count: not_aired_episodes_count;
show: show;
anime_type: anime_type;
static createFrom(source: any = {}) {
return new SimklAnime(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.last_watched_at = source["last_watched_at"];
this.status = source["status"];
this.user_rating = source["user_rating"];
this.last_watched = source["last_watched"];
this.next_to_watch = source["next_to_watch"];
this.watched_episodes_count = source["watched_episodes_count"];
this.total_episodes_count = source["total_episodes_count"];
this.not_aired_episodes_count = source["not_aired_episodes_count"];
this.show = source["show"];
this.anime_type = source["anime_type"];
}
}
export class SimklUser {
user: user;
account: account;
connections: connections;
static createFrom(source: any = {}) {
return new SimklUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.user = source["user"];
this.account = source["account"];
this.connections = source["connections"];
}
}
export class SimklWatchListType {
anime: anime;
static createFrom(source: any = {}) {
return new SimklWatchListType(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.anime = source["anime"];
}
}
}
export namespace struct { MediaList main {
export class {
MediaList: main.MediaList;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.MediaList = this.convertValues(source["MediaList"], main.MediaList);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace struct { Node main {
export class {
node: node;
num_recommendations: numRecommendations;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.num_recommendations = source["num_recommendations"];
}
}
export class {
node: node;
relation_type: relationType;
relation_type_formatted: relationTypeFormatted;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.relation_type = source["relation_type"];
this.relation_type_formatted = source["relation_type_formatted"];
}
}
}
export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" ts_type:\"medium\""; Large string "json:\"large\" ts_type:\"large\"" } "json:\"main_picture\" ts_type:\"mainPicture\"" } "json:\"node\" ts_type:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
export class {
node: node;
list_status: listStatus;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.node = source["node"];
this.list_status = source["list_status"];
}
}
}
export namespace struct { NumListUsers int "json:\"num_list_users\" ts_type:\"numListUsers\""; Status struct { Watching main {
export class {
num_list_users: numListUsers;
Status: struct { Watching main.;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.num_list_users = source["num_list_users"];
this.Status = this.convertValues(source["Status"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
export class {
status: status;
score: score;
num_episodes_watched: numEpisodesWatched;
is_rewatching: isRewatching;
updated_at: updatedAt;
start_date: startDate;
finish_date: finishDate;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.score = source["score"];
this.num_episodes_watched = source["num_episodes_watched"];
this.is_rewatching = source["is_rewatching"];
this.updated_at = source["updated_at"];
this.start_date = source["start_date"];
this.finish_date = source["finish_date"];
}
}
}
export namespace struct { Watching main {
export class {
watching: string;
completed: string;
on_hold: string;
dropped: string;
plan_to_watch: string;
static createFrom(source: any = {}) {
return new (source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.watching = source["watching"];
this.completed = source["completed"];
this.on_hold = source["on_hold"];
this.dropped = source["dropped"];
this.plan_to_watch = source["plan_to_watch"];
}
}
}
-24
View File
@@ -1,24 +0,0 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}
-330
View File
@@ -1,330 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void
// Notification types
export interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
export interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
export interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
// Initializes the notification service for the application.
// This must be called before sending any notifications.
export function InitializeNotifications(): Promise<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
// Sends a notification with action buttons. Requires a registered category.
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
// Registers a notification category that can be used with SendNotificationWithActions.
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
// Removes a notification by its identifier (cross-platform convenience function).
export function RemoveNotification(identifier: string): Promise<void>;
-298
View File
@@ -1,298 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}
export function InitializeNotifications() {
return window.runtime.InitializeNotifications();
}
export function CleanupNotifications() {
return window.runtime.CleanupNotifications();
}
export function IsNotificationAvailable() {
return window.runtime.IsNotificationAvailable();
}
export function RequestNotificationAuthorization() {
return window.runtime.RequestNotificationAuthorization();
}
export function CheckNotificationAuthorization() {
return window.runtime.CheckNotificationAuthorization();
}
export function SendNotification(options) {
return window.runtime.SendNotification(options);
}
export function SendNotificationWithActions(options) {
return window.runtime.SendNotificationWithActions(options);
}
export function RegisterNotificationCategory(category) {
return window.runtime.RegisterNotificationCategory(category);
}
export function RemoveNotificationCategory(categoryId) {
return window.runtime.RemoveNotificationCategory(categoryId);
}
export function RemoveAllPendingNotifications() {
return window.runtime.RemoveAllPendingNotifications();
}
export function RemovePendingNotification(identifier) {
return window.runtime.RemovePendingNotification(identifier);
}
export function RemoveAllDeliveredNotifications() {
return window.runtime.RemoveAllDeliveredNotifications();
}
export function RemoveDeliveredNotification(identifier) {
return window.runtime.RemoveDeliveredNotification(identifier);
}
export function RemoveNotification(identifier) {
return window.runtime.RemoveNotification(identifier);
}
+4 -33
View File
@@ -3,49 +3,20 @@ module AniTrack
go 1.25.0 go 1.25.0
require ( require (
github.com/99designs/keyring v1.2.2
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.19.0
github.com/wailsapp/wails/v2 v2.15.0 github.com/wailsapp/wails/v3 v3.0.0-beta.20
github.com/zalando/go-keyring v0.2.8
) )
require ( require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/labstack/echo/v4 v4.15.2 // indirect
github.com/labstack/gommon v0.5.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.39.0 // indirect
) )
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod
+12 -80
View File
@@ -1,70 +1,27 @@
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/dvsekhvalnov/jose2go v1.8.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -75,37 +32,12 @@ github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/wailsapp/wails/v3 v3.0.0-beta.20 h1:AKKrRGMSqzJET1W0Jt9v+f0oOqM9Amsi7LKMM6XnSOE=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/wailsapp/wails/v3 v3.0.0-beta.20/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.15.0 h1:u7cHK+UesZOlYxyJxfYLteaCPhws6UsZoDdqUejuX6Q=
github.com/wailsapp/wails/v2 v2.15.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+43 -27
View File
@@ -2,45 +2,61 @@ package main
import ( import (
"embed" "embed"
"log"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
) )
//go:embed all:frontend/dist //go:embed all:frontend/dist
var assets embed.FS var assets embed.FS
func main() { func main() {
// Create an instance of the app structure // The service is created after the application (it needs the app
app := NewApp() // reference), so the single-instance callback closes over the variable
// and resolves it at call time. A second instance can only launch once
// this one is running, by which point svc is always set.
var svc *App
// Create application with options // Create application with options
err := wails.Run(&options.App{ app := application.New(application.Options{
Title: "AniTrack", Name: "AniTrack",
Width: 1024, Description: "Track anime watchlists across AniList, MyAnimeList, and Simkl",
Height: 768, Assets: application.AssetOptions{
AssetServer: &assetserver.Options{ Handler: application.AssetFileServerFS(assets),
Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, SingleInstance: &application.SingleInstanceOptions{
OnStartup: app.startup, UniqueID: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b",
SingleInstanceLock: &options.SingleInstanceLock{ OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
UniqueId: "49c93b6d-663d-4b7a-9cb0-8a469ea9182b", svc.onSecondInstanceLaunch(data)
OnSecondInstanceLaunch: app.onSecondInstanceLaunch, },
}, },
Bind: []interface{}{ Linux: application.LinuxOptions{
app, ProgramName: "AniTrack",
},
Linux: &linux.Options{
Icon: []byte("./build/AniTrack.png"),
WindowIsTranslucent: false,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
ProgramName: "AniTrack",
}, },
}) })
if err != nil {
println("Error:", err.Error()) svc = NewApp(app)
app.RegisterService(application.NewService(svc))
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)
} }
} }
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env sh
# Project-attached wrapper around `make release` so you can run:
# ./release 1.6.7 (or) ./release v1.6.7 (or) ./release AniTrack-1.6.7
# instead of:
# make release VERSION=1.6.7
# Lives in the repo (no machine-specific alias needed). Tags are plain
# versions (1.6.7) to match wails.json productVersion.
set -eu
[ "$#" -ge 1 ] || { echo "Usage: ./release 1.6.7" >&2; exit 1; }
# Normalize "v1.6.7" or "AniTrack-1.6.7" down to plain "1.6.7" (the Makefile
# rejects anything that is not plain semver, so this just saves typing).
VERSION="${1#v}"
VERSION="${VERSION#AniTrack-}"
exec make release "VERSION=${VERSION}"
+8 -7
View File
@@ -1,17 +1,18 @@
{ {
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "AniTrack", "name": "AniTrack",
"outputfilename": "AniTrack", "frontend": {
"frontend:install": "npm install", "dir": "./frontend",
"frontend:build": "npm run build", "install": "npm install",
"frontend:dev:watcher": "npm run dev", "build": "npm run build",
"frontend:dev:serverUrl": "auto", "dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"author": { "author": {
"name": "John O'Keefe", "name": "John O'Keefe",
"email": "admin@linuxhg.com" "email": "admin@linuxhg.com"
}, },
"info": { "info": {
"productName": "AniTrack", "productName": "AniTrack",
"productVersion": "1.5.5" "productVersion": "1.99.0"
} }
} }