Compare commits

...
12 Commits
Author SHA1 Message Date
John O'Keefe 2ab5a080a9 chore(release): bump version to 2.0.0
Release / release (push) Successful in 3m20s
2026-09-16 16:56:31 -04:00
John O'Keefe 60ff980809 fix(frontend): header swap breakpoint down to mobile width
- all 16 min-[950px] instances (the only file using the custom
  breakpoint) become standard sm:, so inline search survives the
  whole tablet range and the hamburger appears only once search
  leaves at true mobile width; avatar present throughout either way
- the top-bar search wrapper was also missing its base hidden, which
  rendered search twice below the breakpoint; fixed in the same pass
2026-09-16 16:30:35 -04:00
John O'Keefe e2c8ba10ce fix(frontend): submit-time data flow (rating scale, table reactivity)
- rating submitted doubled: the * 2 belonged to the retired 0-5
  slider scale while StarInput already works in backend 0-10 units,
  so every rating arrived doubled (9 became 18, both backends
  rejected it); form value now passes through unscaled, display and
  words map already native 0-10
- StarInput gains an optional name prop rendering a hidden input,
  restoring the rating form field the old slider exposed; Rating
  passes name="rating"
- table updates go immutable (new array every path): in-place
  mutation with the same reference never reliably invalidated the
  Svelte 5 $derived, which left the grid stale after successful
  submits; replace-or-append semantics unchanged
- per-page dropdown reads the event value synchronously (Svelte 5
  nulls currentTarget after dispatch, crashing the old
  read-inside-.then)
- detail poster column shares a flex centering context so poster and
  stars align by construction at every width
2026-09-16 16:30:23 -04:00
John O'Keefe 515678f035 fix(frontend): Svelte 5 mount API and route-driven refresh
- loader action used new Component() plus $destroy, both removed in
  Svelte 5 (component_api_invalid_new crashed every refresh click
  before the reload ran); rewritten on mount/unmount with a real
  action destroy for the leaked subscription
- homecoming refresh moves off the legacy $: block, which never
  tracked the runes-backed router.location, onto the sanctioned
  onRouteLoaded callback with identical guard semantics; unused
  router import dropped so the check stays warning-free
2026-09-16 16:30:08 -04:00
John O'Keefe e8a5768a2f docs: record toolchain completion and version-source move
- toolchain item flipped to done-state with the deliberate holds
  named (TypeScript 5.9 peer constraint, runtime lockstep rule)
- Makefile/config records updated for the single-source version flow
  and the wails.json retirement proof
- branch map already corrected in an earlier pass; history above the
  edited lines untouched
2026-09-16 16:29:48 -04:00
John O'Keefe 2a6397b964 refactor!: retire wails.json, config.yml is the version source
- version.go (generated, never hand-edited) carries the compiled-in
  version; app.go drops the wails.json embed plus the gjson dependency
- make release bumps config.yml and regenerates version.go in one
  commit; the re-run-safe guards from the 1.99.1 cycle carry over
- release guard reads config.yml; CI CLI pin and cache key move to
  beta.22 to match the lockstep above
- release wrapper and config NOTE comments updated; deletion proof:
  wails3 build green plus a dev boot reaching the connected state
  with the file absent (the one cold-boot failure reproduced as a
  Vite 8 cold-start vs app retry race, cleared by rerunning warm)
2026-09-16 16:29:35 -04:00
John O'Keefe de808dbb67 chore(deps): Wails v3 beta.20 to beta.22 lockstep
- go.mod, local CLI install, and @wailsio/runtime move together;
  the JS/Go wire protocol must match exactly, so the runtime never
  moves alone
- bindings regen is byte-identical (same 29 methods/IDs), confirming
  the bump is behavior-transparent at the binding layer
- beta.23 exists but is hours old and touches Linux single-instance;
  deliberately deferred to a routine bump, not stacked onto 2.0
- deletes wails.json (staged earlier during retirement proof; the
  version-source move lands in the next commit)
- svelte-check 0/0, vet/tests/builds green, smoke launch clean
2026-09-16 16:29:12 -04:00
John O'Keefe f63563ce16 fix(frontend): star rating hover via StarInput, retire overlay slider
The vendored overlay-slider Stars.svelte never responded under
Svelte 5 - a runes port plus an explicit value/oninput rewrite both
left it inert, which convicted the mechanism rather than its
syntax. Replace it with StarInput.svelte: per-star hover zones with
fractional math (preserves the full 0-10 granularity),
snapshot-and-revert on mouseleave, commit on click, arrow-key
nudging, disabled mode. It reuses the Star SVG leaf so output is
pixel-identical, stays dumb (value in/out, no stores) for the
planned inline-grid editing, and Rating.svelte keeps owning the
words/score math locally. Legacy on:change forwarding and the
slider CSS go with the old file.
2026-09-16 14:00:44 -04:00
John O'Keefe 8d5d8ac2d7 refactor(frontend): flowbite-svelte v1 APIs, bindings-aligned store types
flowbite-svelte v1 turns components into runes: on:click on
components becomes onclick props (native elements keep on:), Modal
footers become {#snippet footer()}, Button["color"] indexing becomes
the exported ButtonProps type, and slate is gone from the color
union so the datepicker greys map to gray.

Stores now use the generated bindings models (what the Go backend
actually returns) instead of hand-written shapes that drifted from
them - nullability of genres/tags/mediaList included. That single
move clears most of the newly surfaced type errors, including the
optional episodes/nextAiringEpisode cluster. The rest is strictness
debt the old red check masked: definite-assignment on
subscribe-fed lets, null guards on querySelector/match results and
date parsing (two of which fixed latent throw-on-garbage paths),
aria-labels on icon-only buttons, the completed default-data
literal, and a shared flex centering context for the detail poster
column so poster and stars align by construction at every width.
2026-09-16 14:00:17 -04:00
John O'Keefe 59b81396f9 refactor(frontend): Svelte 5 mount API, router v5, dependency-free table
main.ts uses mount() instead of the legacy constructor. App.svelte
follows svelte-spa-router v5: the removed loc store becomes
router.location, while wrap/push/link keep their v4 call shapes.

AnimeTable.svelte drops svelte-headless-table and reimplements the
same 11 sortable columns (WebsiteLink id cells, asc/desc
indicators, click toggles through asc/desc/off) with local
$state/$derived - about 120 lines, no dependency, fully covered by
svelte-check instead of the libs view model.
2026-09-16 13:59:42 -04:00
John O'Keefe 4087599629 chore(frontend): Tailwind v4 CSS-first config
Move every tailwind.config.js option into src/style.css: @import,
@plugin flowbite/plugin, @source for the flowbite-svelte dist
(node_modules is gitignored so auto-detection skips the lib),
@theme for the custom primary scale, and @utility container
porting the old center plus responsive padding and max-width
ladders. darkMode media is v4s default so it needs no equivalent.
vite.config.ts gains the tailwindcss() plugin; index.html loses the
dead flowbite.js tag.

style.css also carries two v4-behavior restorations found while
verifying the built CSS: pointer cursor on buttons (v4 preflight
dropped v3s rule) and the container max-width ladder the first
pass at this file omitted.
2026-09-16 13:59:19 -04:00
John O'Keefe 577c7d0d5b chore(frontend): Svelte 5 / Vite 8 / Tailwind CSS 4 dependency set
Move the toolchain to Svelte 5.57, Vite 8.3, vite-plugin-svelte 7.3,
svelte-check 4.7.6, svelte-spa-router 5.1.1, Tailwind 4.3 (plus the
new @tailwindcss/vite plugin), flowbite-svelte 1.33 and flowbite
4.0.2.

Dropped outright: svelte-preprocess (only ever handled lang="ts",
which the vite plugin does natively), svelte-headless-table (peers
svelte ^4, dead end - the one table gets rewritten dependency-free),
postcss/autoprefixer (v4 handles transforms internally) and the
vanilla flowbite.js script tag (no data-* usage anywhere; the Svelte
components are self-contained).

Deliberately pinned, not upgraded: typescript stays 5.9
(svelte-check peers ^5||^6, and 7.x is the native-port rewrite) and
@wailsio/runtime stays 3.0.0-beta.20 (must match the Go framework and
CLI exactly - it moves in lockstep with them, never alone).

Fresh lockfile, zero npm vulnerabilities (was ten).
2026-09-16 13:58:47 -04:00
42 changed files with 2034 additions and 2500 deletions
+14 -13
View File
@@ -12,9 +12,10 @@ run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}"
# 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.
# build/config.yml (and the generated version.go), commits the bump,
# creates an annotated tag carrying the git-cliff notes, and pushes
# commit + tag. This workflow verifies build/config.yml 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.
@@ -49,7 +50,7 @@ jobs:
fetch-depth: 0
ref: ${{ gitea.event.inputs.tag || gitea.ref }}
- name: Guard wails.json matches tag
- name: Guard build/config.yml matches tag
run: |
set -euo pipefail
: "${TAG:?TAG is required}"
@@ -58,21 +59,21 @@ jobs:
NORM="${TAG#v}"
NORM="${NORM#AniTrack-}"
BASE="${NORM%%-*}"
WAILS_VER="$(python3 -c "import json; print(json.load(open('wails.json'))['info']['productVersion'])")"
CFG_VER="$(python3 -c "import re; print(re.search(r'^ version: \"([^\"]*)\"', open('build/config.yml').read(), flags=re.M).group(1))")"
if [ "${NORM}" = "${BASE}" ]; then
if [ "${WAILS_VER}" != "${NORM}" ]; then
echo "::error::wails.json productVersion (${WAILS_VER}) != tag (${NORM}). Bump via ./release ${NORM} first." >&2
if [ "${CFG_VER}" != "${NORM}" ]; then
echo "::error::build/config.yml info.version (${CFG_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
# Pre-release (e.g. 1.6.7-rc1): build/config.yml must match the base version.
if [ "${CFG_VER}" != "${BASE}" ]; then
echo "::error::build/config.yml info.version (${CFG_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}"
echo "Version guard passed: build/config.yml=${CFG_VER} tag=${NORM}"
- name: Set up Go
uses: actions/setup-go@v5
@@ -119,13 +120,13 @@ jobs:
# 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 }}
key: wails3-v3.0.0-beta.22-${{ 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
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.22
- name: Add Go bin to PATH
run: echo "${HOME}/go/bin" >> "${GITHUB_PATH}"
+13 -12
View File
@@ -11,22 +11,26 @@ build:
clean:
rm -rf build/bin/*
# Create a version commit (wails.json bump, committed and pushed) plus an
# Create a version commit (version 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
# verifies build/config.yml 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.
#
# The single version source is build/config.yml info.version. `make release`
# bumps it and regenerates version.go (the compiled-in copy) from it, so the
# two can never drift; both ride the same bump commit.
#
# 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
# Tags are plain versions (1.6.7) to match build/config.yml info.version 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
@@ -35,17 +39,14 @@ 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; }
@command -v python3 >/dev/null 2>&1 || { echo "python3 not found — required to bump versions"; 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(); m=re.search(r'^ version: \"([^\"]*)\"', s, flags=re.M); assert m, 'info.version not found'; print('build/config.yml', m.group(1), '-> $(VERSION)' if m.group(1) != '$(VERSION)' else '(already at version)'); open(p,'w').write(re.sub(r'^ version: \"[^\"]*\"', ' version: \"$(VERSION)\"', s, count=1, flags=re.M))"
@git add wails.json build/config.yml
@echo "Regenerating version.go from build/config.yml..."
@python3 -c "import re; v=re.search(r'^ version: \"([^\"]*)\"', open('build/config.yml').read(), flags=re.M).group(1); open('version.go','w').write('package main\n\n// Code generated by \`make release\` from build/config.yml info.version.\n// DO NOT EDIT.\nconst appVersionString = \"%s\"\n' % v)"
@git add build/config.yml version.go
@git diff --cached --quiet && echo "versions already at $(VERSION), skipping bump commit" || git commit -m "chore(release): bump version to $(VERSION)"
@echo "Generating release notes for $(VERSION)..."
@git tag "$(VERSION)" HEAD && \
+3 -7
View File
@@ -1,16 +1,11 @@
package main
import (
_ "embed"
"strings"
"github.com/tidwall/gjson"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed wails.json
var wailsJSON string
// App struct
type App struct {
app *application.App
@@ -21,9 +16,10 @@ func NewApp(app *application.App) *App {
return &App{app: app}
}
// appVersion reads the product version from wails.json.
// appVersion is the release version, stamped into version.go by
// `make release` from build/config.yml.
func appVersion() string {
return gjson.Get(wailsJSON, "info.productVersion").String()
return appVersionString
}
// appTitle is the window title: "AniTrack <version>".
+3 -2
View File
@@ -1,5 +1,6 @@
# Wails v3 project configuration.
# NOTE: `make release` bumps `info.version` below alongside wails.json.
# NOTE: `info.version` below is the single version source: `make release`
# bumps it and regenerates version.go (the compiled-in copy) from it.
# Never touch the top-level `version: '3'` — that is the config file schema,
# not the app version.
version: '3'
@@ -10,7 +11,7 @@ info:
productIdentifier: "com.linuxhg.anitrack"
description: "Track anime watchlists across AniList, MyAnimeList, and Simkl"
copyright: "John O'Keefe"
version: "1.99.1"
version: "2.0.0"
# Dev mode configuration
dev_mode:
+2 -2
View File
@@ -21,7 +21,7 @@ This document summarizes the `wailsv3` branch (Wails `v2.15.0` → `v3.0.0-beta.
**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).
- `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`. Version flow: single source `build/config.yml:info.version`, `make release` bumps it and regenerates `version.go` (compiled-in copy) in the same commit — `wails.json` retired entirely (proof: `wails3 build` + `wails3 dev` both green without it; a first-boot `dev` failure turned out to be Vite 8 cold-start vs the app retry budget, fixed by rerunning warm, not by the file).
- `.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.
@@ -56,7 +56,7 @@ secret, never in the repo (see `.gitignore`).
**Not yet done — staged after the desktop migration, in order** (each with its home branch, so nothing lands in the wrong place):
1. **Stabilize desktop `v3`:** real-world dogfooding of `main` (logins, watchlist sync, error modals). Merged to `main` as the `1.99.x` beta series while Wails `v3` is still beta upstream.
2. **Frontend toolchain refresh (on `wailsv3`, before the merge):** Svelte 4 → 5, Vite 4 → 8, `vite-plugin-svelte 2 → 7`, Tailwind 3 → 4 — majors deferred intentionally; they pair naturally with `v3`s Svelte 5 templates and touch the same desktop files, so no separate branch.
2. **Frontend toolchain refresh:** done on `wailsv3` Svelte 4.2 → 5.57, Vite 4.5 → 8.3, `vite-plugin-svelte 2 → 7`, Tailwind 3 → 4 (CSS-first), flowbite-svelte 0.46 → 1.33, router 4 → 5. `svelte-check` went 453 errors → 0/0; stores retyped to the generated bindings models; AnimeTable rewritten dependency-free (`svelte-headless-table` peers Svelte 4 only). Deliberately held: TypeScript at 5.9 (`svelte-check` peers `^5||^6`), `@wailsio/runtime` pinned to the Go framework version (lockstep upgrades only).
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.
-4
View File
@@ -8,9 +8,5 @@
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
<script
src="./node_modules/flowbite/dist/flowbite.js"
type="module"
></script>
</body>
</html>
+1465 -2007
View File
File diff suppressed because it is too large Load Diff
+15 -18
View File
@@ -11,26 +11,23 @@
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"@tsconfig/svelte": "^4.0.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.45",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"svelte-headless-table": "^0.18.3",
"svelte-preprocess": "^5.0.3",
"svelte-spa-router": "^4.0.1",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.10",
"tslib": "^2.7.0",
"typescript": "^5.0.0",
"vite": "^4.5.5"
"@sveltejs/vite-plugin-svelte": "^7.3.0",
"@tailwindcss/vite": "^4.3.3",
"@tsconfig/svelte": "^5.0.8",
"svelte": "^5.57.0",
"svelte-check": "^4.7.6",
"svelte-spa-router": "^5.1.1",
"tailwind-merge": "^3.7.0",
"tailwindcss": "^4.3.3",
"tslib": "^2.8.0",
"typescript": "^5.9.3",
"vite": "^8.3.0"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@wailsio/runtime": "3.0.0-beta.20",
"flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16",
"moment": "^2.30.1"
"@wailsio/runtime": "3.0.0-beta.22",
"flowbite": "^4.0.2",
"flowbite-svelte": "^1.33.1",
"moment": "^2.31.0"
}
}
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+25 -17
View File
@@ -21,13 +21,12 @@
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import {App} from "../bindings/AniTrack";
import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte";
onMount(async () => {
let isAniListLoggedIn: boolean;
let isMALLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let isMALLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
@@ -39,26 +38,35 @@
!isSimklLoggedIn && (await CheckIfSimklLoggedInAndSetUser());
});
$: if ($loc?.location === "/" && $watchlistNeedsRefresh) {
(async () => {
if ($aniListLoggedIn && $aniListPrimary) {
await CheckIfAniListLoggedInAndLoadWatchList();
}
if ($malLoggedIn && $malPrimary) {
await App.GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if ($simklLoggedIn && $simklPrimary) {
await App.SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
import { get } from "svelte/store";
watchlistNeedsRefresh.set(false);
})();
// Reloads all watchlists when returning home with the refresh flag set
// (e.g. after saving changes on a detail page). Driven by the router's
// onRouteLoaded callback: the old `$: ... router.location ...` reactive
// statement did not reliably re-fire on navigation under Svelte 5,
// so homecoming refreshes silently never ran.
async function refreshHomeWatchlists(location: string): Promise<void> {
if (location !== "/" || !get(watchlistNeedsRefresh)) return;
if (get(aniListLoggedIn) && get(aniListPrimary)) {
await CheckIfAniListLoggedInAndLoadWatchList();
}
if (get(malLoggedIn) && get(malPrimary)) {
await App.GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if (get(simklLoggedIn) && get(simklPrimary)) {
await App.SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
watchlistNeedsRefresh.set(false);
}
</script>
<Header />
<ErrorModal />
<Router
onRouteLoaded={(detail) => {
void refreshHomeWatchlists(detail.location);
}}
routes={{
"/": Home,
"/anime/:id": wrap({
+39 -23
View File
@@ -11,22 +11,22 @@
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import WebsiteLink from "./WebsiteLink.svelte";
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
import type {
AniListGetSingleAnime,
AniListUpdateVariables,
MALAnime,
MalListStatus,
MALUploadStatus,
SimklAnime,
} from "../../bindings/AniTrack/models";
import Rating from "./Rating.svelte";
import {
convertAniListDateToString,
convertAniListDateToDate,
} from "../helperFunctions/convertAniListDateIn";
import AnimeTable from "./AnimeTable.svelte";
import type {
MALAnime,
MalListStatus,
MALUploadStatus,
} from "../mal/types/MALTypes";
import type { SimklAnime } from "../simkl/types/simklTypes";
import { writable } from "svelte/store";
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import {App} from "../../bindings/AniTrack";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
@@ -35,12 +35,12 @@
import { Badge, Tooltip } from "flowbite-svelte";
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
let isAniListLoggedIn: boolean;
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let currentAniListAnime: AniListGetSingleAnime;
let currentMalAnime: MALAnime;
let currentSimklAnime: SimklAnime;
let isAniListLoggedIn!: boolean;
let isMalLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let currentAniListAnime!: AniListGetSingleAnime;
let currentMalAnime!: MALAnime;
let currentSimklAnime!: SimklAnime;
let submitting = writable(false);
let isSubmitting: boolean;
let submitSuccess = writable(false);
@@ -103,11 +103,15 @@
let finishDate = "";
if (currentMalAnime.my_list_status.start_date !== "") {
const startArray = re.exec(currentMalAnime.my_list_status.start_date);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
if (startArray) {
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
}
}
if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec(currentMalAnime.my_list_status.finish_date);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
if (finishArray) {
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
}
}
AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`,
@@ -165,7 +169,9 @@
for (let field of formData) {
const [key, value] = field;
if (key === "rating") {
submitData.rating = Number(value) * 2;
// Form value is already 0-10 (StarInput works in backend units).
// The old * 2 belonged to the retired 0-5 slider scale.
submitData.rating = Number(value);
continue;
}
if (key === "episodes") {
@@ -180,7 +186,11 @@
submitData.status = startingAnilistStatusOption;
continue;
}
submitData[key] = value;
// The only remaining form field is "notes" (string); anything else
// is ignored rather than assigned into a mistyped slot.
if (key === "notes" && typeof value === "string") {
submitData.notes = value;
}
}
try {
@@ -271,13 +281,17 @@
const startArray = re.exec(
currentMalAnime.my_list_status.start_date,
);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
if (startArray) {
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
}
}
if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec(
currentMalAnime.my_list_status.finish_date,
);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
if (finishArray) {
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
}
}
AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`,
@@ -503,7 +517,7 @@
{title}
</h1>
<div class="grid grid-cols-1 md:grid-cols-10 grid-flow-col gap-4">
<div class="md:col-span-2 space-y-3">
<div class="md:col-span-2 space-y-3 flex flex-col items-center">
<img
class="rounded-lg"
src={currentAniListAnime.data.MediaList.media.coverImage.large}
@@ -525,6 +539,7 @@
<div class="relative flex items-center max-w-[8rem]">
<button
type="button"
aria-label="Decrease episode progress"
id="decrement-button"
data-input-counter-decrement="quantity-input"
on:click={() => {
@@ -583,6 +598,7 @@
/>
<button
type="button"
aria-label="Increase episode progress"
id="increment-button"
data-input-counter-increment="quantity-input"
on:click={() => {
@@ -682,7 +698,7 @@
>
<Datepicker
bind:value={startedAtDate}
color="slate"
color="gray"
dateFormat={{
year: "numeric",
month: "2-digit",
@@ -699,7 +715,7 @@
>
<Datepicker
bind:value={completedAtDate}
color="slate"
color="gray"
dateFormat={{
year: "numeric",
month: "2-digit",
+81 -104
View File
@@ -1,118 +1,95 @@
<script lang="ts">
import {
createRender,
createTable,
Render,
Subscribe,
} from "svelte-headless-table";
// @ts-ignore
import { addSortBy } from "svelte-headless-table/plugins";
import { tableItems } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { TableItem } from "../helperTypes/TableTypes";
import WebsiteLink from "./WebsiteLink.svelte";
//when adding sort here is code { sort: addSortBy() }
const table = createTable(tableItems, { sort: addSortBy() });
type SortKey = keyof TableItem;
type SortOrder = "asc" | "desc" | undefined;
const columns = table.createColumns([
table.column({
header: "Service Id",
cell: ({ value }) => createRender(WebsiteLink, { id: value }),
accessor: "id",
}),
table.column({
header: "Anime Title",
accessor: "title",
}),
table.column({
header: "Service",
accessor: "service",
}),
table.column({
header: "Episode Progress",
accessor: "progress",
}),
table.column({
header: "Status",
accessor: "status",
}),
table.column({
header: "Started At",
accessor: "startedAt",
}),
table.column({
header: "Completed At",
accessor: "completedAt",
}),
table.column({
header: "Rating",
accessor: "score",
}),
table.column({
header: "Repeat",
accessor: "repeat",
}),
table.column({
header: "Notes",
accessor: "notes",
}),
]);
const columns: { key: SortKey; header: string }[] = [
{ key: "id", header: "Service Id" },
{ key: "title", header: "Anime Title" },
{ key: "service", header: "Service" },
{ key: "progress", header: "Episode Progress" },
{ key: "status", header: "Status" },
{ key: "startedAt", header: "Started At" },
{ key: "completedAt", header: "Completed At" },
{ key: "score", header: "Rating" },
{ key: "repeat", header: "Repeat" },
{ key: "notes", header: "Notes" },
];
//add pluginStates when add sort back
const { headerRows, rows, tableAttrs, tableBodyAttrs } =
table.createViewModel(columns);
let sortKey = $state<SortKey | undefined>(undefined);
let sortOrder = $state<SortOrder>(undefined);
function toggleSort(key: SortKey) {
if (sortKey !== key) {
sortKey = key;
sortOrder = "asc";
} else if (sortOrder === "asc") {
sortOrder = "desc";
} else if (sortOrder === "desc") {
sortKey = undefined;
sortOrder = undefined;
} else {
sortOrder = "asc";
}
}
function compareItems(a: TableItem, b: TableItem): number {
if (sortKey === undefined || sortOrder === undefined) return 0;
const av = a[sortKey];
const bv = b[sortKey];
let result: number;
if (typeof av === "number" && typeof bv === "number") {
result = av - bv;
} else {
result = String(av ?? "").localeCompare(String(bv ?? ""));
}
return sortOrder === "asc" ? result : -result;
}
let sortedRows = $derived(
sortKey === undefined ? $tableItems : [...$tableItems].sort(compareItems),
);
</script>
<div class="relative overflow-x-auto rounded-lg mb-5">
<table
class="w-full text-sm text-left rtl:text-right text-gray-400"
{...$tableAttrs}
>
<table class="w-full text-sm text-left rtl:text-right text-gray-400">
<thead class="text-xs uppercase bg-gray-700 text-gray-400">
{#each $headerRows as headerRow (headerRow.id)}
<Subscribe attrs={headerRow.attrs()} let:attrs>
<tr {...attrs}>
{#each headerRow.cells as cell (cell.id)}
<Subscribe
attrs={cell.attrs()}
let:attrs
props={cell.props()}
let:props
>
<th
{...attrs}
on:click={props.sort.toggle}
class:sorted={props.sort.order !==
undefined}
class="px-6 py-3"
>
<div>
<Render of={cell.render()} />
{#if props.sort.order === "asc"}
⬇️
{:else if props.sort.order === "desc"}
⬆️
{/if}
</div>
</th>
</Subscribe>
{/each}
</tr>
</Subscribe>
{/each}
<tr>
{#each columns as column (column.key)}
<th
onclick={() => toggleSort(column.key)}
class:sorted={sortKey === column.key &&
sortOrder !== undefined}
class="px-6 py-3 cursor-pointer"
>
<div>
{column.header}
{#if sortKey === column.key && sortOrder === "asc"}
⬇️
{:else if sortKey === column.key && sortOrder === "desc"}
⬆️
{/if}
</div>
</th>
{/each}
</tr>
</thead>
<tbody {...$tableBodyAttrs}>
{#each $rows as row (row.id)}
<Subscribe attrs={row.attrs()} let:attrs>
<tr {...attrs} class="bg-gray-800 border-gray-700">
{#each row.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<td {...attrs} class="px-6 py-4">
<Render of={cell.render()} />
</td>
</Subscribe>
{/each}
</tr>
</Subscribe>
<tbody>
{#each sortedRows as row (row.id + row.service)}
<tr class="bg-gray-800 border-gray-700">
{#each columns as column (column.key)}
<td class="px-6 py-4">
{#if column.key === "id"}
<WebsiteLink id={row.id} />
{:else}
{row[column.key]}
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
+10 -10
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar } from "flowbite-svelte";
import type { AniListUser } from "../anilist/types/AniListTypes";
import type { AniListUser, MyAnimeListUser, SimklUser } from "../../bindings/AniTrack/models";
import {
aniListLoggedIn,
aniListUser,
@@ -17,16 +17,14 @@
serviceLoggingIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {Application} from "@wailsio/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes";
import {App} from "../../bindings/AniTrack";
import {App} from "../../bindings/AniTrack";
let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser;
let currentSimklUser: SimklUser;
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let isMALLoggedIn!: boolean;
let loggingIn: string[] = [];
aniListUser.subscribe((value) => (currentAniListUser = value));
@@ -38,7 +36,8 @@
serviceLoggingIn.subscribe((value) => (loggingIn = value));
function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown");
const dropdown = document.querySelector("#userDropdown");
if (!dropdown) return;
dropdown.classList.toggle("hidden");
if (!dropdown.classList.contains("hidden")) {
@@ -47,8 +46,9 @@
}
function clickOutside(event: Event): void {
let dropdown = document.querySelector("#userDropdown");
let toggleBtn = document.querySelector("#userDropdownButton");
const dropdown = document.querySelector("#userDropdown");
const toggleBtn = document.querySelector("#userDropdownButton");
if (!dropdown || !toggleBtn) return;
if (
!dropdown.contains(event.target as Node) &&
+15 -14
View File
@@ -2,6 +2,7 @@
import { createEventDispatcher, onMount } from "svelte";
import { fade } from "svelte/transition";
import { Button } from "flowbite-svelte";
import type { ButtonProps } from "flowbite-svelte";
export let value: Date | null = null;
export let defaultDate: Date | null = null;
@@ -19,7 +20,7 @@
export let disabled: boolean = false;
export let required: boolean = false;
export let inputClass: string = "";
export let color: Button["color"] = "primary";
export let color: ButtonProps["color"] = "primary";
export let inline: boolean = false;
export let autohide: boolean = true;
export let showActionButtons: boolean = false;
@@ -47,7 +48,7 @@
});
// Color handling functions
function getFocusRingClass(color: Button["color"]): string {
function getFocusRingClass(color: ButtonProps["color"]): string {
switch (color) {
case "primary":
return "focus:ring-2 focus:ring-primary-400";
@@ -61,14 +62,14 @@
return "focus:ring-2 focus:ring-yellow-400";
case "purple":
return "focus:ring-2 focus:ring-purple-400";
case "slate":
return "focus:ring-2 focus:ring-slate-400";
case "gray":
return "focus:ring-2 focus:ring-gray-400";
default:
return "";
}
}
function getRangeBackgroundClass(color: Button["color"]): string {
function getRangeBackgroundClass(color: ButtonProps["color"]): string {
switch (color) {
case "primary":
return "bg-primary-900";
@@ -82,8 +83,8 @@
return "bg-yellow-900";
case "purple":
return "bg-purple-900";
case "slate":
return "bg-slate-900";
case "gray":
return "bg-gray-900";
default:
return "";
}
@@ -352,7 +353,7 @@
{/if}
<div class="flex items-center justify-between mb-4">
<Button
on:click={() => changeMonth(-1)}
onclick={() => changeMonth(-1)}
{color}
size="sm"
aria-label="Previous month"
@@ -382,7 +383,7 @@
})}
</h3>
<Button
on:click={() => changeMonth(1)}
onclick={() => changeMonth(1)}
{color}
size="sm"
aria-label="Next month"
@@ -424,8 +425,8 @@
: ''} {isInRange(day)
? getRangeBackgroundClass(color)
: ''}"
on:click={() => handleDaySelect(day)}
on:keydown={handleCalendarKeydown}
onclick={() => handleDaySelect(day)}
onkeydown={handleCalendarKeydown}
aria-label={day.toLocaleDateString(locale, {
weekday: "long",
year: "numeric",
@@ -441,13 +442,13 @@
</div>
{#if showActionButtons}
<div class="mt-4 flex justify-between">
<Button on:click={handleToday} {color} size="sm"
<Button onclick={handleToday} {color} size="sm"
>Today</Button
>
<Button on:click={handleClear} color="red" size="sm"
<Button onclick={handleClear} color="red" size="sm"
>Clear</Button
>
<Button on:click={handleApply} {color} size="sm"
<Button onclick={handleApply} {color} size="sm"
>Apply</Button
>
</div>
@@ -61,13 +61,15 @@
dismiss this message to continue with limited functionality.
</p>
</div>
<div slot="footer" class="flex gap-3 justify-end">
{#if $apiError.canRetry}
<Button on:click={handleRetry} class="bg-blue-600 hover:bg-blue-700">
Retry Connection
</Button>
{/if}
<Button on:click={handleDismiss} color="alternative">Dismiss</Button>
</div>
{#snippet footer()}
<div class="flex gap-3 justify-end">
{#if $apiError.canRetry}
<Button onclick={handleRetry} class="bg-blue-600 hover:bg-blue-700">
Retry Connection
</Button>
{/if}
<Button onclick={handleDismiss} color="alternative">Dismiss</Button>
</div>
{/snippet}
</Modal>
{/if}
+11 -10
View File
@@ -13,9 +13,9 @@
import logo from "../assets/images/AniTrackLogo.svg";
import { link } from "svelte-spa-router";
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let isMALLoggedIn!: boolean;
let loggingIn: string[] = [];
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
@@ -34,19 +34,20 @@
>
</div>
<div
class="flex items-center min-[950px]:order-2 space-x-3 min-[950px]:space-x-0 rtl:space-x-reverse"
class="flex items-center sm:order-2 space-x-3 sm:space-x-0 rtl:space-x-reverse"
>
<div class="min-[950px]:block min-[950px]:mr-4">
<div class="hidden sm:block sm:mr-4">
<Search />
</div>
<AvatarMenu />
<button
on:click={() => {
let menu = document.querySelector("#navbar-user");
const menu = document.querySelector("#navbar-user");
if (!menu) return;
menu.classList.toggle("hidden");
}}
type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg sm:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
aria-controls="navbar-user"
aria-expanded="false"
>
@@ -69,11 +70,11 @@
</button>
</div>
<div
class="hidden items-center justify-between w-full pb-4 min-[950px]:pb-0 min-[950px]:flex min-[950px]:w-auto min-[950px]:order-1 border border-gray-700 min-[950px]:border-0 bg-gray-800 min-[950px]:bg-transparent rounded-lg"
class="hidden items-center justify-between w-full pb-4 sm:pb-0 sm:flex sm:w-auto sm:order-1 border border-gray-700 sm:border-0 bg-gray-800 sm:bg-transparent rounded-lg"
id="navbar-user"
>
<ul
class="flex flex-col font-medium pb-6 min-[950px]:p-0 mt-4 min-[950px]:space-x-8 rtl:space-x-reverse min-[950px]:flex-row min-[950px]:mt-0"
class="flex flex-col font-medium pb-6 sm:p-0 mt-4 sm:space-x-8 rtl:space-x-reverse sm:flex-row sm:mt-0"
>
<li>
{#if !isAniListLoggedIn}
@@ -125,7 +126,7 @@
{/if}
</li>
</ul>
<div class="flex justify-center min-[950px]:hidden">
<div class="flex justify-center sm:hidden">
<Search />
</div>
</div>
+12 -10
View File
@@ -7,13 +7,13 @@
watchListPage,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import type { AniListCurrentUserWatchList } from "../../bindings/AniTrack/models";
import {App} from "../../bindings/AniTrack";
let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
watchListPage.subscribe((value) => (page = value));
animePerPage.subscribe((value) => (perPage = value));
@@ -43,14 +43,14 @@
function changeCountPerPage(
e: Event & { currentTarget: HTMLSelectElement },
): void {
App.GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
(result) => {
animePerPage.set(Number(e.currentTarget.value));
watchListPage.set(1);
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
},
);
// Read synchronously: Svelte 5 nulls currentTarget after dispatch.
const count = Number(e.currentTarget.value);
App.GetAniListUserWatchingList(1, count, sort).then((result) => {
animePerPage.set(count);
watchListPage.set(1);
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
});
}
</script>
@@ -151,6 +151,7 @@
<div class="relative flex items-center max-w-[11rem]">
<button
type="button"
aria-label="Previous page"
id="decrement-button"
on:click={() => ChangeWatchListPage(page - 1)}
class={page <= 1
@@ -191,6 +192,7 @@
</div>
<button
type="button"
aria-label="Next page"
id="increment-button"
on:click={() => ChangeWatchListPage(page + 1)}
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage
+6 -31
View File
@@ -1,30 +1,9 @@
<script lang="ts">
import StarRatting from "../star-rating/Stars.svelte";
import StarInput from "../star-rating/StarInput.svelte";
export let score
let { score = $bindable(0) }: { score: number } = $props();
let config = {
readOnly: false,
countStars: 5,
range: {
min: 0,
max: 5,
step: 0.5
},
score: score / 2,
showScore: false,
name: "rating",
scoreFormat: function(){ return `(${this.score.toFixed(0)}/${this.countStars})` },
starConfig: {
size: 32,
fillColor: '#F9ED4F',
strokeColor: "#e2c714",
unfilledColor: '#FFF',
strokeUnfilledColor: '#000'
}
}
const ratingInWords = {
const ratingInWords: Record<number, string> = {
0: "Not Reviewed",
1: "Appalling",
2: "Horrible",
@@ -37,14 +16,10 @@
9: "Great",
10: "Masterpiece",
}
const changeRating = (e: any) => {
score = e.target.valueAsNumber * 2
}
</script>
<div>
<StarRatting bind:config on:change={changeRating}/>
<p>Rating: {config.score * 2}</p>
<p>{ratingInWords[config.score * 2]}</p>
<StarInput bind:value={score} min={0} max={10} step={1} name="rating" />
<p>Rating: {score}</p>
<p>{ratingInWords[score]}</p>
</div>
+1 -1
View File
@@ -50,7 +50,7 @@
{ value: MediaListSort.MediaPopularityDesc, name: "Media Popularity Desc" },
];
let sort: string;
let sort!: string;
aniListSort.subscribe((value) => (sort = value));
console.log(sort);
@@ -6,14 +6,14 @@
loading,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import type { AniListCurrentUserWatchList } from "../../bindings/AniTrack/models";
import { Rating } from "flowbite-svelte";
import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Sort from "../helperComponents/Sort.svelte";
import RefreshWatchListButton from "./RefreshWatchListButton.svelte";
let isAniListLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
@@ -12,7 +12,7 @@
isAniList = id.includes("a-");
isMAL = id.includes("m-");
isSimkl = id.includes("s-");
if (isAniList || isMAL || isSimkl) newId = id.match(re)[1];
if (isAniList || isMAL || isSimkl) newId = id.match(re)![1];
else newId = id;
}
@@ -1,4 +1,4 @@
import type {AniListGetSingleAnime} from "../anilist/types/AniListCurrentUserWatchListType";
import type {AniListGetSingleAnime} from "../../bindings/AniTrack/models";
export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
data: {
@@ -10,23 +10,57 @@ export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
id: 0,
idMal: 0,
title: {
userPreferred: "",
romaji: "",
english: "",
native: "",
},
description: "",
coverImage: {
ExtraLarge: "",
large: "",
Medium: "",
Color: "",
},
BannerImage: "",
Format: "",
season: "",
seasonYear: 0,
status: "",
episodes: 0,
Duration: 0,
CountryOfOrigin: "",
Source: "",
Synonyms: null,
AverageScore: 0,
MeanScore: 0,
Popularity: 0,
Trending: 0,
Favourites: 0,
relations: {
nodes: null,
},
startDate: {
year: 0,
month: 0,
day: 0,
},
endDate: {
year: 0,
month: 0,
day: 0,
},
nextAiringEpisode: {
airingAt: 0,
timeUntilAiring: 0,
episode: 0,
}
},
airingSchedule: {
nodes: null,
},
genres: [],
tags: [],
isAdult: false,
},
status: "",
startedAt: {
@@ -13,7 +13,7 @@ const convertAniListDateToString = (date: {
) {
return "";
}
const newISODate = new Date(date.year, date.month - 1, date.day);
const newISODate = new Date(date.year, date.month! - 1, date.day);
const newMoment = moment(newISODate);
return newMoment.format("MM-DD-YYYY");
};
@@ -31,7 +31,7 @@ const convertAniListDateToDate = (date: {
) {
return null;
}
return new Date(date.year, date.month - 1, date.day);
return new Date(date.year, date.month! - 1, date.day);
};
export { convertAniListDateToString, convertAniListDateToDate };
@@ -14,6 +14,13 @@ const convertDateStringToAniList = (date: string): AnilistDate => {
}
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
const newDate = re.exec(date);
if (newDate === null) {
return {
year: 0,
month: 0,
day: 0,
};
}
return {
year: Number(newDate[1]),
month: Number(newDate[2]),
+21 -10
View File
@@ -1,18 +1,29 @@
import { mount, unmount } from 'svelte';
import Spinner from '../helperComponents/Spinner.svelte';
export default (node: any, loading: any) => {
let Spin: any
loading.subscribe((loading: any) => {
if(loading){
Spin = new Spinner({
let app: any;
const unsubscribe = loading.subscribe((isLoading: any) => {
if (isLoading && !app) {
app = mount(Spinner, {
target: node,
intro: true
})
} else {
if(Spin){
Spin?.$destroy?.()
Spin = undefined;
});
} else if (!isLoading && app) {
const current = app;
app = undefined;
// Teardown must never break the caller.
void unmount(current).catch(() => {});
}
});
return {
destroy() {
unsubscribe();
if (app) {
const current = app;
app = undefined;
void unmount(current).catch(() => {});
}
}
})
};
}
@@ -3,19 +3,19 @@
import { tableItems } from "./GlobalVariablesAndHelperFunctions.svelte"
export function AddAnimeServiceToTable(animeItem: TableItem) {
// Always return a NEW array: in-place mutation with the same
// reference does not reliably invalidate $derived consumers
// under Svelte 5, which left the table stale after submits.
tableItems.update((table) => {
if (table.length === 0) {
table.push(animeItem)
} else {
for (const [index, tableItem] of table.entries()) {
if(tableItem.service === animeItem.service) {
table[index] = animeItem
return table
}
}
table.push(animeItem)
const index = table.findIndex(
(tableItem) => tableItem.service === animeItem.service,
);
if (index === -1) {
return [...table, animeItem];
}
return table
return table.map((tableItem, i) =>
i === index ? animeItem : tableItem,
);
})
}
</script>
@@ -16,7 +16,7 @@
let isAniListPrimary: boolean;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
@@ -1,7 +1,7 @@
<script lang="ts" context="module">
import {App} from "../../bindings/AniTrack";
import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte"
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { MyAnimeListUser } from "../../bindings/AniTrack/models";
let isMalPrimary: boolean
malPrimary.subscribe(value => isMalPrimary = value)
@@ -3,22 +3,16 @@
import type {
AniListCurrentUserWatchList,
AniListGetSingleAnime,
} from "../anilist/types/AniListCurrentUserWatchListType.js";
import { writable } from "svelte/store";
import type {
SimklAnime,
SimklUser,
SimklWatchList,
} from "../simkl/types/simklTypes";
import {
type AniListUser,
MediaListSort,
} from "../anilist/types/AniListTypes";
import type {
AniListUser,
MALAnime,
MALWatchlist,
MyAnimeListUser,
} from "../mal/types/MALTypes";
SimklAnime,
SimklUser,
SimklWatchListType,
} from "../../bindings/AniTrack/models";
import { writable } from "svelte/store";
import { MediaListSort } from "../anilist/types/AniListTypes";
import type { TableItems } from "../helperTypes/TableTypes";
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
@@ -28,7 +22,7 @@
export let simklLoggedIn = writable(false);
export let malLoggedIn = writable(false);
export const serviceLoggingIn = writable([] as string[]);
export let simklWatchList = writable({} as SimklWatchList);
export let simklWatchList = writable({} as SimklWatchListType);
export let aniListPrimary = writable(true);
export let simklPrimary = writable(false);
export let malPrimary = writable(false);
@@ -50,12 +44,12 @@
let isAniListPrimary: boolean;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
let aniWatchlist: AniListCurrentUserWatchList;
let currentAniListAnime: AniListGetSingleAnime;
let currentAniListAnime!: AniListGetSingleAnime;
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMalLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
+3 -2
View File
@@ -1,8 +1,9 @@
import './style.css'
import { mount } from 'svelte'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')
const app = mount(App, {
target: document.getElementById('app')!
})
export default app
+1 -1
View File
@@ -12,7 +12,7 @@
import loader from "../helperFunctions/loader";
let isAniListPrimary: boolean;
let isAniListLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
+142
View File
@@ -0,0 +1,142 @@
<!-- Dumb reusable star-rating input. No stores, no backend knowledge:
`value` is written live on hover (reverted on leave unless committed)
and set on click/keyboard; the parent owns what the value means
(words, persistence). Reuses the Star leaf, so output is pixel-identical
to the old overlay-slider version without the overlay. -->
<script lang="ts">
import Star from './components/Star.svelte';
type StarColors = {
size: number;
fillColor: string;
strokeColor: string;
unfilledColor: string;
strokeUnfilledColor: string;
};
let {
value = $bindable(0),
min = 0,
max = 5,
step = 0.5,
count = 5,
disabled = false,
name,
starConfig = {
size: 32,
fillColor: '#F9ED4F',
strokeColor: '#BB8511',
unfilledColor: '#FFF',
strokeUnfilledColor: '#000'
},
onchange,
}: {
value?: number;
min?: number;
max?: number;
step?: number;
count?: number;
disabled?: boolean;
// When set, renders a hidden input so the value submits with a
// surrounding <form> via FormData (the old slider exposed
// name="rating" the same way).
name?: string;
starConfig?: StarColors;
onchange?: (value: number) => void;
} = $props();
// Value at hover entry; restored on leave unless committed. Null = idle.
let baseline: number | null = $state(null);
function clamp(v: number): number {
return Math.min(max, Math.max(min, v));
}
function snap(v: number): number {
const snapped = min + Math.round((v - min) / step) * step;
return clamp(Math.round(snapped * 1e6) / 1e6);
}
// Continuous value for cursor position within star `index`.
function valueAt(index: number, fraction: number): number {
if (index === 0 && fraction < 0.2) return min;
const f = fraction >= 0.5 ? 1 : 0.5;
return snap(min + ((index + f) / count) * (max - min));
}
function fractionOf(e: MouseEvent, el: HTMLElement): number {
const rect = el.getBoundingClientRect();
if (rect.width <= 0) return 1;
return Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
}
function fillOf(index: number): number {
const per = (max - min) / count;
return Math.min(1, Math.max(0, (value - min - index * per) / per));
}
function onHoverMove(e: MouseEvent, el: HTMLElement, index: number) {
if (disabled) return;
if (baseline === null) baseline = value;
value = valueAt(index, fractionOf(e, el));
}
function onHoverLeave() {
if (disabled) return;
if (baseline !== null) {
value = baseline;
baseline = null;
}
}
function commit(v: number) {
if (disabled) return;
value = clamp(v);
baseline = value;
onchange?.(value);
}
</script>
<div
class="stars-container"
role="group"
aria-label="Star rating"
onmouseleave={onHoverLeave}
>
<div class="stars">
{#if name !== undefined}
<input type="hidden" name={name} value={value} />
{/if}
{#each Array(count) as _, i}
<button
type="button"
class="flex items-center justify-center p-0 border-0 bg-transparent"
disabled={disabled}
aria-label={`Rate ${i + 1} out of ${count}`}
onmousemove={(e) => onHoverMove(e, e.currentTarget, i)}
onclick={() => commit(value)}
onkeydown={(e) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault();
commit(value + step);
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault();
commit(value - step);
}
}}
>
<Star
id={`star-${i}`}
readOnly={disabled}
starConfig={starConfig}
fillPercentage={fillOf(i)}
/>
</button>
{/each}
</div>
</div>
<style>
.stars-container{ position: relative; display: flex; align-items: center; justify-content: center; gap: .5rem; }
.stars{ display: flex; align-items: center; justify-content: center; gap: .5rem; }
</style>
-63
View File
@@ -1,63 +0,0 @@
<!-- Originally from @ernane/svelte-star-rating. Wanted to give credit but could not use from the library without causing program crash. -->
<script>
import Star from './components/Star.svelte';
export let config = {
readOnly: false,
countStars: 5,
range: { min: 0, max: 5, step: 0.001 },
score: 0.0,
showScore: true,
name: "stars",
scoreFormat: function(){ return `(${this.score.toFixed(0)}/${this.countStars})` },
starConfig: {
size: 30,
fillColor: '#F9ED4F',
strokeColor: "#BB8511",
unfilledColor: '#FFF',
strokeUnfilledColor: '#000'
}
}
</script>
<section class="stars-container">
<div class="range-stars">
<div class="stars">
{#each Array(config.countStars) as star, id}
{#if Math.floor(config.score) === id}
<Star id={config.name + id} readOnly={config.readOnly} starConfig={config.starConfig} fillPercentage={config.score - Math.floor(config.score)}/>
{:else if Math.floor(config.score) > id}
<Star id={config.name + id} readOnly={config.readOnly} starConfig={config.starConfig} fillPercentage={1}/>
{:else}
<Star id={config.name + id} readOnly={config.readOnly} starConfig={config.starConfig} fillPercentage={0}/>
{/if}
{/each}
</div>
<input name={config.name}
class="slider"
type="range"
min={config.readOnly ? config.score : config.range.min}
max={config.readOnly ? config.score : config.range.max}
step="{config.range.step}" bind:value={config.score}
on:change
on:click
>
</div>
{#if config.showScore}
<span class="show-score" style="font-size: {config.starConfig.size/2}px;">
{#if config.scoreFormat}
{config.scoreFormat()}
{:else}
({((config.score/config.countStars)*100).toFixed(2)}%)
{/if}
</span>
{/if}
</section>
<style>
.stars-container{ position: relative; display: flex; align-items: center; justify-content: center; gap: .5rem; }
.range-stars{ position: relative; }
.stars{ display: flex; align-items: center; justify-content: center; gap: .5rem; }
.slider{ opacity: 0; cursor: pointer; position: absolute; top: 0; left: 0; right: 0; height: 100%; }
.show-score{ user-select: none; color: #888 }
</style>
+57 -3
View File
@@ -1,6 +1,60 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin 'flowbite/plugin';
@source "../node_modules/flowbite-svelte/dist";
@theme {
--color-primary-50: #FFF5F2;
--color-primary-100: #FFF1EE;
--color-primary-200: #FFE4DE;
--color-primary-300: #FFD5CC;
--color-primary-400: #FFBCAD;
--color-primary-500: #FE795D;
--color-primary-600: #EF562F;
--color-primary-700: #EB4F27;
--color-primary-800: #CC4522;
--color-primary-900: #A5371B;
}
@utility container {
width: 100%;
margin-inline: auto;
padding-inline: 1rem;
@media (width >= theme(--breakpoint-sm)) {
max-width: theme(--breakpoint-sm);
padding-inline: 2rem;
}
@media (width >= theme(--breakpoint-md)) {
max-width: theme(--breakpoint-md);
}
@media (width >= theme(--breakpoint-lg)) {
max-width: theme(--breakpoint-lg);
padding-inline: 4rem;
}
@media (width >= theme(--breakpoint-xl)) {
max-width: theme(--breakpoint-xl);
padding-inline: 5rem;
}
@media (width >= theme(--breakpoint-2xl)) {
max-width: theme(--breakpoint-2xl);
padding-inline: 6rem;
}
}
@layer base {
/* Tailwind v4 removed the v3 preflight rule that gave buttons a
pointer cursor. Restore it (disabled buttons keep the default). */
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
html {
background-color: rgba(27, 38, 54, 1);
-7
View File
@@ -1,7 +0,0 @@
import sveltePreprocess from 'svelte-preprocess'
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}
-44
View File
@@ -1,44 +0,0 @@
import flowbitePlugin from 'flowbite/plugin'
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
"./node_modules/flowbite/**/*.{html,js,svelte,ts}",
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
],
plugins: [ flowbitePlugin ],
darkMode: 'media',
theme: {
container: {
center: true,
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
'2xl': '6rem',
},
},
extend: {
colors: {
// flowbite-svelte
primary: {
50: '#FFF5F2',
100: '#FFF1EE',
200: '#FFE4DE',
300: '#FFD5CC',
400: '#FFBCAD',
500: '#FE795D',
600: '#EF562F',
700: '#EB4F27',
800: '#CC4522',
900: '#A5371B'
},
}
}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'
import tailwindcss from '@tailwindcss/vite'
import wails from '@wailsio/runtime/plugins/vite'
// https://vitejs.dev/config/
@@ -9,5 +10,5 @@ export default defineConfig({
port: Number(process.env.WAILS_VITE_PORT) || 5173,
strictPort: true,
},
plugins: [svelte(), wails('./bindings')]
plugins: [tailwindcss(), svelte(), wails('./bindings')]
})
+1 -4
View File
@@ -3,8 +3,7 @@ module AniTrack
go 1.25.0
require (
github.com/tidwall/gjson v1.19.0
github.com/wailsapp/wails/v3 v3.0.0-beta.20
github.com/wailsapp/wails/v3 v3.0.0-beta.22
github.com/zalando/go-keyring v0.2.8
)
@@ -16,7 +15,5 @@ require (
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/sys v0.46.0 // indirect
)
+2 -8
View File
@@ -26,14 +26,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/wailsapp/wails/v3 v3.0.0-beta.20 h1:AKKrRGMSqzJET1W0Jt9v+f0oOqM9Amsi7LKMM6XnSOE=
github.com/wailsapp/wails/v3 v3.0.0-beta.20/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0=
github.com/wailsapp/wails/v3 v3.0.0-beta.22 h1:GsdqwRB7nESQyeZKVv16DLIS30xCLMlZP/8UhgKnHYc=
github.com/wailsapp/wails/v3 v3.0.0-beta.22/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+1 -1
View File
@@ -4,7 +4,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.
# versions (1.6.7) to match build/config.yml info.version.
set -eu
[ "$#" -ge 1 ] || { echo "Usage: ./release 1.6.7" >&2; exit 1; }
+5
View File
@@ -0,0 +1,5 @@
package main
// Code generated by `make release` from build/config.yml info.version.
// DO NOT EDIT.
const appVersionString = "2.0.0"
-18
View File
@@ -1,18 +0,0 @@
{
"name": "AniTrack",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"author": {
"name": "John O'Keefe",
"email": "admin@linuxhg.com"
},
"info": {
"productName": "AniTrack",
"productVersion": "1.99.1"
}
}