From e1ddf864fb7844a4cc0cf8d9d2edfb94aed0f179 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Fri, 16 Oct 2026 04:37:12 -0700 Subject: [PATCH] feat: vendor Viewer.js reproducibly and automate release updates Refs #169. Pin and verify pristine distribution assets, upgrade to 1.15.2, and add tested maintainer tooling plus desktop and touch browser checks. --- .github/dependabot.yml | 12 ++ .github/workflows/viewerjs-check.yml | 41 ++++++ .github/workflows/viewerjs-update.yml | 83 ++++++++++++ .gitignore | 5 + README.md | 3 + tests/vendor/vendor-viewerjs.test.mjs | 107 +++++++++++++++ tools/vendor/README.md | 76 +++++++++++ tools/vendor/package-lock.json | 68 ++++++++++ tools/vendor/package.json | 17 +++ tools/vendor/playwright.config.mjs | 16 +++ tools/vendor/tests/viewer.spec.mjs | 183 ++++++++++++++++++++++++++ tools/vendor/update-pr.md | 8 ++ tools/vendor/update-viewerjs.mjs | 34 +++++ tools/vendor/vendor-viewerjs.mjs | 70 ++++++++++ web/lib/viewerjs/LICENSE | 21 +++ web/lib/viewerjs/provenance.json | 21 +++ web/lib/viewerjs/viewer.min.css | 6 +- web/lib/viewerjs/viewer.min.js | 6 +- 18 files changed, 771 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/viewerjs-check.yml create mode 100644 .github/workflows/viewerjs-update.yml create mode 100644 tests/vendor/vendor-viewerjs.test.mjs create mode 100644 tools/vendor/README.md create mode 100644 tools/vendor/package-lock.json create mode 100644 tools/vendor/package.json create mode 100644 tools/vendor/playwright.config.mjs create mode 100644 tools/vendor/tests/viewer.spec.mjs create mode 100644 tools/vendor/update-pr.md create mode 100644 tools/vendor/update-viewerjs.mjs create mode 100644 tools/vendor/vendor-viewerjs.mjs create mode 100644 web/lib/viewerjs/LICENSE create mode 100644 web/lib/viewerjs/provenance.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ba88677..ec57ad7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -46,3 +46,15 @@ updates: update-types: - "minor" - "patch" + + # Maintainer browser test dependencies; Viewer.js itself has an asset-aware updater. + - package-ecosystem: "npm" + directory: "/tools/vendor" + schedule: + interval: "weekly" + allow: + - dependency-name: "@playwright/test" + labels: + - "dependencies" + commit-message: + prefix: "deps(vendor-tools)" diff --git a/.github/workflows/viewerjs-check.yml b/.github/workflows/viewerjs-check.yml new file mode 100644 index 0000000..46d7a28 --- /dev/null +++ b/.github/workflows/viewerjs-check.yml @@ -0,0 +1,41 @@ +name: Viewer.js vendor and compatibility + +on: + pull_request: + paths: + - 'tools/vendor/**' + - 'tests/vendor/**' + - 'tests/js/**' + - 'web/js/**' + - 'web/lib/**' + - '.github/workflows/viewerjs-*.yml' + push: + branches: [main] + paths: + - 'tools/vendor/**' + - 'tests/vendor/**' + - 'tests/js/**' + - 'web/js/**' + - 'web/lib/**' + - '.github/workflows/viewerjs-*.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + viewerjs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '24' + cache: npm + cache-dependency-path: tools/vendor/package-lock.json + - run: npm ci --prefix tools/vendor --ignore-scripts + - run: npm run --prefix tools/vendor check + - run: npm run --prefix tools/vendor test + - run: node --test tests/js/*.test.js + - run: npx --prefix tools/vendor playwright install --with-deps chromium + - run: npm run --prefix tools/vendor test:browser diff --git a/.github/workflows/viewerjs-update.yml b/.github/workflows/viewerjs-update.yml new file mode 100644 index 0000000..77fba66 --- /dev/null +++ b/.github/workflows/viewerjs-update.yml @@ -0,0 +1,83 @@ +name: Update vendored Viewer.js + +on: + schedule: + - cron: '30 7 * * *' + workflow_dispatch: + inputs: + version: + description: 'Stable version to vendor; leave blank for the latest npm release' + required: false + type: string + +permissions: + contents: read + +concurrency: + group: viewerjs-update + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + actions: write + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + - uses: actions/setup-node@v7 + with: + node-version: '24' + cache: npm + cache-dependency-path: tools/vendor/package-lock.json + - run: npm ci --prefix tools/vendor --ignore-scripts + - name: Update the dependency and regenerate assets + env: + VIEWER_VERSION: ${{ inputs.version }} + run: | + if [ -n "$VIEWER_VERSION" ]; then + npm run --prefix tools/vendor update -- --version "$VIEWER_VERSION" + else + npm run --prefix tools/vendor update + fi + - name: Detect changes + id: changes + run: | + if git diff --quiet -- tools/vendor/package.json tools/vendor/package-lock.json web/lib/viewerjs; then + echo 'changed=false' >> "$GITHUB_OUTPUT" + else + echo 'changed=true' >> "$GITHUB_OUTPUT" + fi + - if: steps.changes.outputs.changed == 'true' + run: | + npm run --prefix tools/vendor check + npm run --prefix tools/vendor test + node --test tests/js/*.test.js + npx --prefix tools/vendor playwright install --with-deps chromium + npm run --prefix tools/vendor test:browser + - name: Open the update PR + if: steps.changes.outputs.changed == 'true' + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + run: | + version=$(node -p "require('./web/lib/viewerjs/provenance.json').version") + branch="deps/viewerjs-$version" + existing=$(gh pr list --repo "$GITHUB_REPOSITORY" --state all --head "$branch" --json number --jq 'length') + if [ "$existing" != '0' ]; then + echo "A PR already exists for Viewer.js $version; leaving it for review." + exit 0 + fi + remote=$(git ls-remote origin "refs/heads/$branch") + if [ -z "$remote" ]; then + git switch -c "$branch" + git add tools/vendor/package.json tools/vendor/package-lock.json web/lib/viewerjs + git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' commit -m "deps: update vendored Viewer.js to $version" + git push origin "HEAD:refs/heads/$branch" + fi + gh pr create --repo "$GITHUB_REPOSITORY" --base "$BASE_BRANCH" --head "$branch" --title "deps: update vendored Viewer.js to $version" --body-file tools/vendor/update-pr.md + gh workflow run viewerjs-check.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" diff --git a/.gitignore b/.gitignore index c335b78..c29d97c 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,8 @@ cython_debug/ # PyPI configuration file .pypirc + +# Maintainer-only JavaScript tooling and browser test artifacts +node_modules/ +test-results/ +playwright-report/ diff --git a/README.md b/README.md index 62cbfe3..ce4c2b1 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,9 @@ The comprehensive gallery system provides complete management of all ComfyUI out _Powered by [ViewerJS](https://github.com/fengyuanchen/viewerjs) for professional image viewing capabilities._ +Viewer.js is vendored locally. See [Viewer.js maintenance](tools/vendor/README.md) +for reproducible updates, verification, and automated update PRs. + ### 🔬 ComfyUI Workflow Analysis - **PNG Metadata Parsing**: Advanced PNG chunk analysis to extract embedded ComfyUI data diff --git a/tests/vendor/vendor-viewerjs.test.mjs b/tests/vendor/vendor-viewerjs.test.mjs new file mode 100644 index 0000000..5ffb502 --- /dev/null +++ b/tests/vendor/vendor-viewerjs.test.mjs @@ -0,0 +1,107 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createHash } from 'node:crypto'; +import { syncViewer } from '../../tools/vendor/vendor-viewerjs.mjs'; +import { updateViewer } from '../../tools/vendor/update-viewerjs.mjs'; + +function fixture(t) { + const root = mkdtempSync(join(tmpdir(), 'pm-vendor-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const tooling = join(root, 'tools/vendor'); + const dependency = join(tooling, 'node_modules/viewerjs'); + mkdirSync(join(dependency, 'dist'), { recursive: true }); + writeFileSync(join(tooling, 'package.json'), JSON.stringify({ devDependencies: { viewerjs: '1.15.2' } })); + writeFileSync(join(tooling, 'package-lock.json'), JSON.stringify({ packages: { + 'node_modules/viewerjs': { version: '1.15.2', resolved: 'https://registry.npmjs.org/viewerjs/-/viewerjs-1.15.2.tgz', integrity: 'sha512-fixture' }, + } })); + writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: 'viewerjs', version: '1.15.2', license: 'MIT' })); + for (const name of ['viewer.min.js', 'viewer.min.css']) { + writeFileSync(join(dependency, 'dist', name), `/*! Viewer.js v1.15.2 */\n${name}`); + } + writeFileSync(join(dependency, 'LICENSE'), 'The MIT License (MIT)\nCopyright Chen Fengyuan\n'); + return { root, tooling, dependency, destination: join(root, 'web/lib/viewerjs') }; +} + +test('sync copies pristine assets and license and records locked provenance deterministically', t => { + const f = fixture(t); + syncViewer({ root: f.root }); + const first = readFileSync(join(f.destination, 'provenance.json'), 'utf8'); + const record = JSON.parse(first); + assert.equal(record.version, '1.15.2'); + assert.equal(record.integrity, 'sha512-fixture'); + for (const [target, source] of Object.entries({ 'viewer.min.js': 'dist/viewer.min.js', 'viewer.min.css': 'dist/viewer.min.css', LICENSE: 'LICENSE' })) { + const original = readFileSync(join(f.dependency, source)); + assert.deepEqual(readFileSync(join(f.destination, target)), original); + assert.equal(record.files[target].sha256, createHash('sha256').update(original).digest('hex')); + } + syncViewer({ root: f.root }); + assert.equal(readFileSync(join(f.destination, 'provenance.json'), 'utf8'), first); + syncViewer({ root: f.root, check: true }); +}); + +test('check rejects drift without overwriting local changes', t => { + const f = fixture(t); + syncViewer({ root: f.root }); + writeFileSync(join(f.destination, 'viewer.min.js'), 'local edit'); + assert.throws(() => syncViewer({ root: f.root, check: true }), /viewer.min.js/); + assert.equal(readFileSync(join(f.destination, 'viewer.min.js'), 'utf8'), 'local edit'); + rmSync(join(f.destination, 'LICENSE')); + assert.throws(() => syncViewer({ root: f.root, check: true }), /LICENSE/); +}); + +test('sync refuses mismatched installed versions and unpinned dependencies', t => { + const f = fixture(t); + writeFileSync(join(f.dependency, 'package.json'), JSON.stringify({ name: 'viewerjs', version: '1.11.6', license: 'MIT' })); + assert.throws(() => syncViewer({ root: f.root }), /version/i); + writeFileSync(join(f.tooling, 'package.json'), JSON.stringify({ devDependencies: { viewerjs: '^1.15.2' } })); + assert.throws(() => syncViewer({ root: f.root }), /exact|pin/i); +}); + +test('sync validates every upstream asset before writing anything', t => { + const f = fixture(t); + writeFileSync(join(f.dependency, 'dist/viewer.min.css'), '/*! Viewer.js v1.11.6 */'); + assert.throws(() => syncViewer({ root: f.root }), /version|banner/i); + assert.throws(() => readFileSync(join(f.destination, 'viewer.min.js')), /ENOENT/); +}); + +test('updater resolves the stable release and installs it exactly with lifecycle scripts disabled', t => { + const f = fixture(t); + const calls = []; + let synced = false; + const run = (args, options) => { + calls.push({ args, options }); + return args[0] === 'view' ? '"1.15.3"' : ''; + }; + assert.equal(updateViewer({ root: f.root, run, sync: () => { synced = true; } }), '1.15.3'); + assert.deepEqual(calls[1].args, ['install', '--save-dev', '--save-exact', '--ignore-scripts', 'viewerjs@1.15.3']); + assert.equal(calls[1].options.cwd, f.tooling); + assert.equal(synced, true); +}); + +test('updater is a no-op at the current release and rejects invalid version arguments', t => { + const f = fixture(t); + const run = () => { throw new Error('must not install'); }; + assert.equal(updateViewer({ root: f.root, version: '1.15.2', run }), null); + for (const version of ['latest', '1.15.3-beta.1', '1.15.3;anything', '--global']) { + assert.throws(() => updateViewer({ root: f.root, version, run }), /version/i); + } +}); + +test('failed installation never copies vendor assets', t => { + const f = fixture(t); + let synced = false; + assert.throws(() => updateViewer({ root: f.root, version: '1.15.3', run: () => { throw new Error('offline'); }, sync: () => { synced = true; } }), /offline/); + assert.equal(synced, false); +}); + +test('updater accepts a single registry result array and rejects ambiguous metadata', t => { + const f = fixture(t); + const calls = []; + const run = args => { calls.push(args); return '["1.15.2"]'; }; + assert.equal(updateViewer({ root: f.root, run }), null); + assert.equal(calls.length, 1); + assert.throws(() => updateViewer({ root: f.root, run: () => '["1.15.2", "1.15.3"]' }), /version/i); +}); diff --git a/tools/vendor/README.md b/tools/vendor/README.md new file mode 100644 index 0000000..66f94f5 --- /dev/null +++ b/tools/vendor/README.md @@ -0,0 +1,76 @@ +# Viewer.js maintenance + +ComfyUI serves the committed files in `web/lib/viewerjs/`. Node and npm are only +needed by maintainers and CI. Keep upstream files pristine; application behavior +belongs in `web/js/`, and theme overrides belong in `comfyui-theme.css`. + +## Install and verify + +Use Node 24 or newer. From the repository root: + +```sh +npm ci --prefix tools/vendor --ignore-scripts +npm run --prefix tools/vendor check +npm run --prefix tools/vendor test +node --test tests/js/*.test.js +npx --prefix tools/vendor playwright install chromium +npm run --prefix tools/vendor test:browser +``` + +On Linux, use `playwright install --with-deps chromium` when browser system +libraries are missing. The browser tests use synthetic images and mock the +ComfyUI API boundary; they run production viewer methods and the committed +Viewer.js JS/CSS, without a running ComfyUI instance. + +`check` is read-only and fails if any JS, CSS, license, or provenance file differs +from the installed package. The manifest, lockfile, installed package, and asset +version banners must agree. The lockfile records npm's archive integrity; +`provenance.json` records the upstream source, package integrity, and SHA-256 of +each copied file. A normal `npm ci --ignore-scripts` verifies archive integrity. + +## Update + +```sh +npm run --prefix tools/vendor update +# Or select a specific stable version: +npm run --prefix tools/vendor update -- --version 1.15.2 +``` + +The command installs an exact version with lifecycle scripts disabled and copies +`dist/viewer.min.js`, `dist/viewer.min.css`, and `LICENSE`. It writes deterministic +provenance with no timestamps. Repeat the checks above and commit the manifest, +lockfile, and all vendor files together. To regenerate files from the current +pin without changing versions, run `npm run --prefix tools/vendor sync`. + +## Automated update PRs + +`Update vendored Viewer.js` checks npm's stable release daily at 07:30 UTC and +supports **Actions → Update vendored Viewer.js → Run workflow**, with an optional +version. It tests the generated update before opening `deps/viewerjs-X.Y.Z`. +Existing open or closed PRs for that release are left alone. A failed test does +not publish a branch or PR. Changes require review; the workflow never merges +or approves PRs. + +The organization and repository must enable **Actions → General → Workflow +permissions → Allow GitHub Actions to create and approve pull requests**. +Keep default workflow permissions read-only. Only the update job requests +`contents: write`, `pull-requests: write`, and `actions: write`. + +The workflow uses `GITHUB_TOKEN`; no personal token is needed. Because bot-token +pushes do not normally trigger other workflows, it explicitly dispatches the +read-only `Viewer.js vendor and compatibility` workflow on the update branch. +Both workflow files must be merged into the default branch before scheduling or +manual dispatch becomes available. + +## Compatibility checklist + +Check gallery grid/list views, navigation across 100-image windows, metadata, +slideshow/fullscreen, keyboard navigation, Escape and focus cleanup. Check the +admin gallery/filmstrip and tag viewer. On touch devices, test long swipes, +short drags, and pinch/rotate gestures. Confirm the ComfyUI theme remains usable. + +The gallery keeps its bounded viewer and custom slideshow. Upstream navbar +windowing is not a replacement for bounding the supplied image list: native +slideshow playback can still preload every supplied original. Review new +preloading defaults, gesture behavior, CSS changes, and any adapter use of +Viewer.js internal methods before adopting another release. diff --git a/tools/vendor/package-lock.json b/tools/vendor/package-lock.json new file mode 100644 index 0000000..8cba944 --- /dev/null +++ b/tools/vendor/package-lock.json @@ -0,0 +1,68 @@ +{ + "name": "promptmanager-vendor-tools", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "promptmanager-vendor-tools", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "1.64.1", + "viewerjs": "1.15.2" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/viewerjs": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.15.2.tgz", + "integrity": "sha512-CYEqSFfPUSnmn1PjrhYxXOScaUC7n8+NpSqI+MrOSM8wcKPe35MlwQYwksuD5rjFkYv/ePmY5NsUa51uRNN2jA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tools/vendor/package.json b/tools/vendor/package.json new file mode 100644 index 0000000..42e47b9 --- /dev/null +++ b/tools/vendor/package.json @@ -0,0 +1,17 @@ +{ + "name": "promptmanager-vendor-tools", + "private": true, + "version": "1.0.0", + "description": "Maintainer-only tools for reproducible browser dependencies", + "scripts": { + "sync": "node vendor-viewerjs.mjs", + "check": "node vendor-viewerjs.mjs --check", + "update": "node update-viewerjs.mjs", + "test": "node --test ../../tests/vendor/*.test.mjs", + "test:browser": "playwright test --config playwright.config.mjs" + }, + "devDependencies": { + "@playwright/test": "1.64.1", + "viewerjs": "1.15.2" + } +} diff --git a/tools/vendor/playwright.config.mjs b/tools/vendor/playwright.config.mjs new file mode 100644 index 0000000..4e26aed --- /dev/null +++ b/tools/vendor/playwright.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + testMatch: '*.spec.mjs', + fullyParallel: true, + workers: 2, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + reporter: 'list', + use: { browserName: 'chromium', headless: true, trace: 'retain-on-failure' }, + projects: [ + { name: 'desktop', use: { viewport: { width: 1280, height: 800 } } }, + { name: 'touch', use: { viewport: { width: 800, height: 700 }, hasTouch: true, isMobile: true } }, + ], +}); diff --git a/tools/vendor/tests/viewer.spec.mjs b/tools/vendor/tests/viewer.spec.mjs new file mode 100644 index 0000000..dbc18fd --- /dev/null +++ b/tools/vendor/tests/viewer.spec.mjs @@ -0,0 +1,183 @@ +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { repositoryRoot as root } from '../vendor-viewerjs.mjs'; + +const source = file => readFileSync(join(root, file), 'utf8'); + +async function waitForImage(page, index) { + await expect.poll(() => page.evaluate(() => window.gallery.viewer?.viewed)).toBe(true); + await expect.poll(() => page.evaluate(() => window.gallery.lastMetadata)).toContain(`/original/${index}.svg`); +} + +async function loadGallery(page) { + await page.addScriptTag({ content: source('web/js/gallery.js') + '\nwindow.TestGallery = GalleryManager;' }); + await page.evaluate(() => { + const manager = window.gallery = Object.create(TestGallery.prototype); + Object.assign(manager, { + viewer: null, viewMode: 'grid', + images: Array.from({ length: 2000 }, (_, i) => ({ filename: `${i}.svg`, + url: `http://viewer.test/original/${i}.svg`, thumbnail_url: `http://viewer.test/thumb/${i}.svg`, + relative_path: `${i}.svg`, size: 100, modified_time: 1 })), + getSettings: () => ({ showImageInfo: true }), + loadImageMetadata(url, content) { this.lastMetadata = url; content.textContent = url; }, + }); + manager.renderGallery(); + }); +} + +test.beforeEach(async ({ page }) => { + page.viewerErrors = []; + page.on('pageerror', error => page.viewerErrors.push(error.message)); + await page.route('http://viewer.test/**', route => route.request().resourceType() === 'document' + ? route.fulfill({ contentType: 'text/html', body: '

' }) + : route.fulfill({ contentType: 'image/svg+xml', body: '' })); + await page.goto('http://viewer.test/'); + await page.addStyleTag({ path: join(root, 'web/lib/tailwind/comfyui-theme.css') }); + await page.addStyleTag({ path: join(root, 'web/lib/tailwind/styles.css') }); + await page.addStyleTag({ path: join(root, 'web/lib/viewerjs/viewer.min.css') }); + await page.addStyleTag({ content: '.hidden{display:none} #galleryGrid{display:grid;grid-template-columns:repeat(8,1fr)} #galleryGrid img{width:100px;height:100px}' }); + for (const file of ['web/lib/viewerjs/viewer.min.js', 'web/js/html-escape.js', 'web/js/image-helpers.js', 'web/js/api-client.js']) { + await page.addScriptTag({ path: join(root, file) }); + } +}); + +test.afterEach(async ({ page }) => { + expect(page.viewerErrors).toEqual([]); +}); + +test('gallery retains cards and navigates across bounded windows with correct metadata', async ({ page }) => { + await loadGallery(page); + await page.locator('[data-image-index="1500"]').click(); + await waitForImage(page, 1500); + expect(await page.evaluate(() => gallery.viewer.length)).toBeLessThanOrEqual(100); + expect(await page.evaluate(() => { + const first = document.querySelector('.image-item'); + gallery.images.push({ ...gallery.images[0], filename: 'appended.svg' }); + gallery.renderGallery({ from: 2000 }); + return first === document.querySelector('.image-item'); + })).toBe(true); + await page.evaluate(() => gallery.viewer.view(99)); + await waitForImage(page, 1549); + await page.keyboard.press('ArrowRight'); + await waitForImage(page, 1550); + await page.keyboard.press('ArrowLeft'); + await waitForImage(page, 1549); + await page.keyboard.press('Escape'); + await expect.poll(() => page.evaluate(() => gallery.viewer.isShown)).toBe(false); + await expect(page.locator('#metadata-sidebar')).toHaveCount(0); + await page.locator('#outside').focus(); + await expect(page.locator('#outside')).toBeFocused(); +}); + +test('gallery slideshow advances without preloading the collection and cleans up on close', async ({ page }) => { + await loadGallery(page); + await page.evaluate(() => gallery.openViewer(1500)); + await waitForImage(page, 1500); + await page.clock.install(); + await page.locator('[data-viewer-action="play"]').click(); + expect(await page.evaluate(() => gallery.slideshowPlaying)).toBe(true); + await expect(page.locator('.viewer-player img')).toHaveCount(0); + await page.clock.fastForward(5000); + await waitForImage(page, 1501); + await page.locator('[data-viewer-action="play"]').click(); + expect(await page.evaluate(() => gallery.slideshowPlaying)).toBe(false); + await page.keyboard.press('Escape'); + await expect.poll(() => page.evaluate(() => gallery.viewer.isShown)).toBe(false); + await page.evaluate(() => gallery.destroyViewer()); + await expect(page.locator('.viewer-container')).toHaveCount(0); +}); + +test('admin gallery and filmstrip use the real viewer events and destroy temporary containers', async ({ page }) => { + // Exercise the production class without starting the ComfyUI dashboard bootstrap. + const adminSource = source('web/js/admin.js'); + await page.addScriptTag({ content: adminSource.slice(0, adminSource.indexOf(' function startAdmin()')) + '\nwindow.TestAdmin = PromptAdmin;' }); + await page.evaluate(async () => { + const admin = window.admin = Object.create(TestAdmin.prototype); + const images = Array.from({ length: 3 }, (_, i) => ({ filename: `${i}.svg`, url: `http://viewer.test/original/${i}.svg`, size: 100, created_at: 1 })); + Object.assign(admin, { + prompts: [{ id: 1, text: 'test' }], + getImageUrl: image => image.url, + loadFilmStripImages: async () => images, + addMetadataSidebar() {}, + loadMetadataForImage(image) { this.lastMetadata = image.src; }, + }); + await admin.openFilmStripViewer(1, 1); + }); + await expect.poll(() => page.evaluate(() => admin.lastMetadata)).toContain('/1.svg'); + await page.keyboard.press('ArrowRight'); + await expect.poll(() => page.evaluate(() => admin.lastMetadata)).toContain('/2.svg'); + await page.keyboard.press('Escape'); + await expect(page.locator('#filmStripViewerContainer')).toHaveCount(0); + await expect(page.locator('.viewer-container')).toHaveCount(0); + await page.evaluate(() => admin.renderGallery([{ filename: 'a.svg', url: 'http://viewer.test/original/0.svg', size: 100, created_at: 1 }], 1)); + await expect.poll(() => page.evaluate(() => Boolean(admin.galleryViewer))).toBe(true); + await page.evaluate(() => admin.galleryViewer.show()); + await expect.poll(() => page.evaluate(() => admin.galleryViewer.viewed)).toBe(true); + await page.keyboard.press('Escape'); + await expect.poll(() => page.evaluate(() => admin.galleryViewer.isShown)).toBe(false); + await page.evaluate(() => admin.galleryViewer.destroy()); +}); + +test('tag viewer opens at the selected image and cleans up on Escape', async ({ page }) => { + await page.addScriptTag({ path: join(root, 'web/js/tags-page.js') }); + await page.evaluate(async () => { + const tags = Object.create(TagsPageManager.prototype); + tags.admin = { getImageUrl: image => image.url }; + await tags.openImageViewer({ images: [0, 1, 2].map(i => ({ filename: `${i}.svg`, url: `http://viewer.test/original/${i}.svg` })), image_count: 3 }, 1); + }); + await expect(page.locator('.viewer-canvas img')).toHaveAttribute('src', /\/1.svg$/); + await page.keyboard.press('ArrowRight'); + await expect(page.locator('.viewer-canvas img')).toHaveAttribute('src', /\/2.svg$/); + await page.keyboard.press('Escape'); + await expect(page.locator('.viewer-container')).toHaveCount(0); +}); + +test('touch swipes navigate the gallery and restore short drags', async ({ page, isMobile }) => { + test.skip(!isMobile, 'Touch interaction project'); + await loadGallery(page); + await page.evaluate(() => gallery.openViewer(1500)); + await waitForImage(page, 1500); + await page.locator('.metadata-collapse-btn').click(); + await expect(page.locator('.metadata-sidebar')).toHaveClass(/collapsed/); + const session = await page.context().newCDPSession(page); + const width = await page.evaluate(() => gallery.viewer.viewerData.width); + const swipe = async distance => { + await session.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: width * 0.9, y: 220 }] }); + for (let step = 1; step <= 8; step++) { + await session.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: width * 0.9 - distance * step / 8, y: 220 }] }); + } + await session.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); + }; + await swipe(width * 0.8); + await waitForImage(page, 1501); + const initialX = await page.evaluate(() => gallery.viewer.imageData.x); + await swipe(width * 0.1); + await waitForImage(page, 1501); + await expect.poll(() => page.evaluate(() => gallery.viewer.imageData.x)).toBe(initialX); +}); + +test('touch pinch zoom and rotation remain usable', async ({ page, isMobile }) => { + test.skip(!isMobile, 'Touch interaction project'); + await loadGallery(page); + await page.evaluate(() => gallery.openViewer(1500)); + await waitForImage(page, 1500); + await page.locator('.metadata-collapse-btn').click(); + await expect(page.locator('.metadata-sidebar')).toHaveClass(/collapsed/); + const session = await page.context().newCDPSession(page); + const points = (radius, angle = 0) => [-1, 1].map((sign, id) => ({ + id, x: 400 + sign * radius * Math.cos(angle), y: 220 + sign * radius * Math.sin(angle), + })); + await session.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: points(30) }); + for (let step = 1; step <= 6; step++) { + await session.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: points(30 + step * 8) }); + } + await session.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); + await expect.poll(() => page.evaluate(() => gallery.viewer.imageData.ratio > 1.2)).toBe(true); + await session.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: points(70) }); + for (let step = 1; step <= 8; step++) { + await session.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: points(70, Math.PI / 2 * step / 8) }); + } + await session.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); + await expect.poll(() => page.evaluate(() => Math.abs(gallery.viewer.imageData.rotate))).toBe(90); +}); diff --git a/tools/vendor/update-pr.md b/tools/vendor/update-pr.md new file mode 100644 index 0000000..95094f3 --- /dev/null +++ b/tools/vendor/update-pr.md @@ -0,0 +1,8 @@ +Update the exact Viewer.js dependency pin and lockfile, pristine JavaScript and CSS, MIT license, and provenance hashes together. + +Validation completed before this PR was opened: +- Vendored assets match the installed locked package. +- Vendoring tooling and existing frontend tests pass. +- Chromium desktop and touch compatibility checks pass for the gallery, admin, and tag viewers. + +Review the [upstream releases](https://github.com/fengyuanchen/viewerjs/releases) for behavioral changes. This PR requires review and is not automatically merged. diff --git a/tools/vendor/update-viewerjs.mjs b/tools/vendor/update-viewerjs.mjs new file mode 100644 index 0000000..43ca1d8 --- /dev/null +++ b/tools/vendor/update-viewerjs.mjs @@ -0,0 +1,34 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { repositoryRoot, stableVersion, syncViewer } from './vendor-viewerjs.mjs'; + +function npm(args, options) { + if (!process.env.npm_execpath) throw new Error('Run the updater through npm run update'); + return execFileSync(process.execPath, [process.env.npm_execpath, ...args], options); +} + +export function updateViewer({ root = repositoryRoot, version, run = npm, sync = syncViewer } = {}) { + const cwd = join(root, 'tools/vendor'); + const current = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')).devDependencies.viewerjs; + const result = version ?? JSON.parse(run(['view', 'viewerjs', 'version', '--json'], { cwd, encoding: 'utf8' })); + const target = Array.isArray(result) && result.length === 1 ? result[0] : result; + if (typeof target !== 'string' || !stableVersion.test(target)) throw new Error('Expected a stable Viewer.js version such as 1.15.2'); + if (target === current) return null; + run(['install', '--save-dev', '--save-exact', '--ignore-scripts', `viewerjs@${target}`], { cwd, stdio: 'inherit' }); + sync({ root }); + return target; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { + const args = process.argv.slice(2); + if (args.length && (args.length !== 2 || args[0] !== '--version')) throw new Error('Usage: npm run update -- [--version X.Y.Z]'); + const version = updateViewer({ version: args[1] }); + console.log(version ? `Updated Viewer.js to ${version}` : 'Viewer.js is already current'); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/tools/vendor/vendor-viewerjs.mjs b/tools/vendor/vendor-viewerjs.mjs new file mode 100644 index 0000000..c690897 --- /dev/null +++ b/tools/vendor/vendor-viewerjs.mjs @@ -0,0 +1,70 @@ +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +export const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url)); +export const stableVersion = /^\d+\.\d+\.\d+$/; +const readJson = path => JSON.parse(readFileSync(path, 'utf8')); + +export function syncViewer({ root = repositoryRoot, check = false } = {}) { + const tooling = join(root, 'tools/vendor'); + const manifest = readJson(join(tooling, 'package.json')); + const version = manifest.devDependencies.viewerjs; + if (!stableVersion.test(version)) throw new Error('Viewer.js must use an exact stable version pin'); + const locked = readJson(join(tooling, 'package-lock.json')).packages['node_modules/viewerjs']; + const installed = join(tooling, 'node_modules/viewerjs'); + const info = readJson(join(installed, 'package.json')); + if (locked?.version !== version || info.version !== version) { + throw new Error('Viewer.js version mismatch: run npm ci --ignore-scripts in tools/vendor'); + } + if (!locked.integrity || !locked.resolved || info.name !== 'viewerjs' || info.license !== 'MIT') { + throw new Error('Viewer.js package identity, integrity, or license is missing'); + } + const files = {}; + const output = new Map(); + for (const [target, source] of Object.entries({ + 'viewer.min.js': 'dist/viewer.min.js', + 'viewer.min.css': 'dist/viewer.min.css', + LICENSE: 'LICENSE', + })) { + const content = readFileSync(join(installed, source)); + if (target !== 'LICENSE' && !content.toString('utf8').includes(`Viewer.js v${version}\n`) && + !content.toString('utf8').includes(`Viewer.js v${version} */`)) { + throw new Error(`Viewer.js version banner mismatch in ${source}`); + } + files[target] = { source, sha256: createHash('sha256').update(content).digest('hex') }; + output.set(target, content); + } + output.set('provenance.json', Buffer.from(JSON.stringify({ + name: info.name, + version, + source: `https://github.com/fengyuanchen/viewerjs/tree/v${version}`, + resolved: locked.resolved, + integrity: locked.integrity, + files, + }, null, 2) + '\n')); + const destination = join(root, 'web/lib/viewerjs'); + if (check) { + const drift = [...output].filter(([name, content]) => { + try { return !readFileSync(join(destination, name)).equals(content); } + catch (error) { if (error.code === 'ENOENT') return true; throw error; } + }).map(([name]) => name); + if (drift.length) throw new Error(`Vendored Viewer.js drift: ${drift.join(', ')}. Run npm run sync in tools/vendor.`); + } else { + mkdirSync(destination, { recursive: true }); + for (const [name, content] of output) writeFileSync(join(destination, name), content); + } + return version; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { + const args = process.argv.slice(2); + if (args.some(arg => arg !== '--check')) throw new Error('Usage: vendor-viewerjs.mjs [--check]'); + console.log(`Viewer.js ${syncViewer({ check: args.includes('--check') })}: ${args.includes('--check') ? 'verified' : 'synced'}`); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/web/lib/viewerjs/LICENSE b/web/lib/viewerjs/LICENSE new file mode 100644 index 0000000..4ca99ac --- /dev/null +++ b/web/lib/viewerjs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2015-present Chen Fengyuan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/web/lib/viewerjs/provenance.json b/web/lib/viewerjs/provenance.json new file mode 100644 index 0000000..2c2b689 --- /dev/null +++ b/web/lib/viewerjs/provenance.json @@ -0,0 +1,21 @@ +{ + "name": "viewerjs", + "version": "1.15.2", + "source": "https://github.com/fengyuanchen/viewerjs/tree/v1.15.2", + "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.15.2.tgz", + "integrity": "sha512-CYEqSFfPUSnmn1PjrhYxXOScaUC7n8+NpSqI+MrOSM8wcKPe35MlwQYwksuD5rjFkYv/ePmY5NsUa51uRNN2jA==", + "files": { + "viewer.min.js": { + "source": "dist/viewer.min.js", + "sha256": "490580be4988dff2e05cd9c81c4161b82ce0c3715d268fadf8f7193853f40031" + }, + "viewer.min.css": { + "source": "dist/viewer.min.css", + "sha256": "24a43eb0b188c824bb5b32e4d95c08b6162ec01eaa709ca8f0823d9512d0913d" + }, + "LICENSE": { + "source": "LICENSE", + "sha256": "eb917650781dcf06a60b97cba9b05bf583931ec2d311d8a5548a8dca90a57e0f" + } + } +} diff --git a/web/lib/viewerjs/viewer.min.css b/web/lib/viewerjs/viewer.min.css index f3fad6b..6d91e29 100644 --- a/web/lib/viewerjs/viewer.min.css +++ b/web/lib/viewerjs/viewer.min.css @@ -1,9 +1,9 @@ /*! - * Viewer.js v1.11.6 + * Viewer.js v1.15.2 * https://fengyuanchen.github.io/viewerjs * * Copyright 2015-present Chen Fengyuan * Released under the MIT license * - * Date: 2023-09-17T03:16:35.830Z - */.viewer-close:before,.viewer-flip-horizontal:before,.viewer-flip-vertical:before,.viewer-fullscreen-exit:before,.viewer-fullscreen:before,.viewer-next:before,.viewer-one-to-one:before,.viewer-play:before,.viewer-prev:before,.viewer-reset:before,.viewer-rotate-left:before,.viewer-rotate-right:before,.viewer-zoom-in:before,.viewer-zoom-out:before{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAAUCAYAAABWOyJDAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAQPSURBVHic7Zs/iFxVFMa/0U2UaJGksUgnIVhYxVhpjDbZCBmLdAYECxsRFBTUamcXUiSNncgKQbSxsxH8gzAP3FU2jY0kKKJNiiiIghFlccnP4p3nPCdv3p9778vsLOcHB2bfveeb7955c3jvvNkBIMdxnD64a94GHMfZu3iBcRynN7zAOI7TG15gHCeeNUkr8zaxG2lbYDYsdgMbktBsP03jdQwljSXdtBhLOmtjowC9Mg9L+knSlcD8TNKpSA9lBpK2JF2VdDSR5n5J64m0qli399hNFMUlpshQii5jbXTbHGviB0nLNeNDSd9VO4A2UdB2fp+x0eCnaXxWXGA2X0au/3HgN9P4LFCjIANOJdrLr0zzZ+BEpNYDwKbpnQMeAw4m8HjQtM6Z9qa917zPQwFr3M5KgA6J5rTJCdFZJj9/lyvGhsDvwFNVuV2MhhjrK6b9bFiE+j1r87eBl4HDwCF7/U/k+ofAX5b/EXBv5JoLMuILzf3Ap6Z3EzgdqHMCuF7hcQf4HDgeoHnccncqdK/TvSDWffFXI/exICY/xZyqc6XLWF1UFZna4gJ7q8BsRvgd2/xXpo6P+D9dfT7PpECtA3cnWPM0GXGFZh/wgWltA+cDNC7X+AP4GzjZQe+k5dRxuYPeiuXU7e1qwLpDz7dFjXKRaSwuMLvAlG8zZlG+YmiK1HoFqT7wP2z+4Q45TfEGcMt01xLoNZEBTwRqD4BLpnMLeC1A41UmVxsXgXeBayV/Wx20rpTyrpnWRft7p6O/FdqzGrDukPNtkaMoMo3FBdBSQMOnYBCReyf05s126fU9ytfX98+mY54Kxnp7S9K3kj6U9KYdG0h6UdLbkh7poFXMfUnSOyVvL0h6VtIXHbS6nOP+s/Zm9mvyXW1uuC9ohZ72E9uDmXWLJOB1GxsH+DxPftsB8B6wlGDN02TAkxG6+4D3TWsbeC5CS8CDFce+AW500LhhOW2020TRjK3b21HEmgti9m0RonxbdMZeVzV+/4tF3cBpP7E9mKHNL5q8h5g0eYsCMQz0epq8gQrwMXAgcs0FGXGFRcB9wCemF9PkbYqM/Bas7fxLwNeJPdTdpo4itQti8lPMqTpXuozVRVXPpbHI3KkNTB1NfkL81j2mvhDp91HgV9MKuRIqrykj3WPq4rHyL+axj8/qGPmTqi6F9YDlHOvJU6oYcTsh/TYSzWmTE6JT19CtLTJt32D6CmHe0eQn1O8z5AXgT4sx4Vcu0/EQecMydB8z0hUWkTd2t4CrwNEePqMBcAR4mrBbwyXLPWJa8zrXmmLEhNBmfpkuY2102xxrih+pb+ieAb6vGhuA97UcJ5KR8gZ77K+99xxeYBzH6Q3/Z0fHcXrDC4zjOL3hBcZxnN74F+zlvXFWXF9PAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-size:280px;color:transparent;display:block;font-size:0;height:20px;line-height:0;width:20px}.viewer-zoom-in:before{background-position:0 0;content:"Zoom In"}.viewer-zoom-out:before{background-position:-20px 0;content:"Zoom Out"}.viewer-one-to-one:before{background-position:-40px 0;content:"One to One"}.viewer-reset:before{background-position:-60px 0;content:"Reset"}.viewer-prev:before{background-position:-80px 0;content:"Previous"}.viewer-play:before{background-position:-100px 0;content:"Play"}.viewer-next:before{background-position:-120px 0;content:"Next"}.viewer-rotate-left:before{background-position:-140px 0;content:"Rotate Left"}.viewer-rotate-right:before{background-position:-160px 0;content:"Rotate Right"}.viewer-flip-horizontal:before{background-position:-180px 0;content:"Flip Horizontal"}.viewer-flip-vertical:before{background-position:-200px 0;content:"Flip Vertical"}.viewer-fullscreen:before{background-position:-220px 0;content:"Enter Full Screen"}.viewer-fullscreen-exit:before{background-position:-240px 0;content:"Exit Full Screen"}.viewer-close:before{background-position:-260px 0;content:"Close"}.viewer-container{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;bottom:0;direction:ltr;font-size:0;left:0;line-height:0;overflow:hidden;position:absolute;right:0;top:0;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.viewer-container ::-moz-selection,.viewer-container::-moz-selection{background-color:transparent}.viewer-container ::selection,.viewer-container::selection{background-color:transparent}.viewer-container:focus{outline:0}.viewer-container img{display:block;height:auto;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.viewer-canvas{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.viewer-canvas>img{height:auto;margin:15px auto;max-width:90%!important;width:auto}.viewer-footer{bottom:0;left:0;overflow:hidden;position:absolute;right:0;text-align:center}.viewer-navbar{background-color:rgba(0,0,0,.5);overflow:hidden}.viewer-list{box-sizing:content-box;height:50px;margin:0;overflow:hidden;padding:1px 0}.viewer-list>li{color:transparent;cursor:pointer;float:left;font-size:0;height:50px;line-height:0;opacity:.5;overflow:hidden;transition:opacity .15s;width:30px}.viewer-list>li:focus,.viewer-list>li:hover{opacity:.75}.viewer-list>li:focus{outline:0}.viewer-list>li+li{margin-left:1px}.viewer-list>.viewer-loading{position:relative}.viewer-list>.viewer-loading:after{border-width:2px;height:20px;margin-left:-10px;margin-top:-10px;width:20px}.viewer-list>.viewer-active,.viewer-list>.viewer-active:focus,.viewer-list>.viewer-active:hover{opacity:1}.viewer-player{background-color:#000;bottom:0;cursor:none;display:none;right:0;z-index:1}.viewer-player,.viewer-player>img{left:0;position:absolute;top:0}.viewer-toolbar>ul{display:inline-block;margin:0 auto 5px;overflow:hidden;padding:6px 3px}.viewer-toolbar>ul>li{background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;float:left;height:24px;overflow:hidden;transition:background-color .15s;width:24px}.viewer-toolbar>ul>li:focus,.viewer-toolbar>ul>li:hover{background-color:rgba(0,0,0,.8)}.viewer-toolbar>ul>li:focus{box-shadow:0 0 3px #fff;outline:0;position:relative;z-index:1}.viewer-toolbar>ul>li:before{margin:2px}.viewer-toolbar>ul>li+li{margin-left:1px}.viewer-toolbar>ul>.viewer-small{height:18px;margin-bottom:3px;margin-top:3px;width:18px}.viewer-toolbar>ul>.viewer-small:before{margin:-1px}.viewer-toolbar>ul>.viewer-large{height:30px;margin-bottom:-3px;margin-top:-3px;width:30px}.viewer-toolbar>ul>.viewer-large:before{margin:5px}.viewer-tooltip{background-color:rgba(0,0,0,.8);border-radius:10px;color:#fff;display:none;font-size:12px;height:20px;left:50%;line-height:20px;margin-left:-25px;margin-top:-10px;position:absolute;text-align:center;top:50%;width:50px}.viewer-title{color:#ccc;display:inline-block;font-size:12px;line-height:1.2;margin:5px 5%;max-width:90%;min-height:14px;opacity:.8;overflow:hidden;text-overflow:ellipsis;transition:opacity .15s;white-space:nowrap}.viewer-title:hover{opacity:1}.viewer-button{-webkit-app-region:no-drag;background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;height:80px;overflow:hidden;position:absolute;right:-40px;top:-40px;transition:background-color .15s;width:80px}.viewer-button:focus,.viewer-button:hover{background-color:rgba(0,0,0,.8)}.viewer-button:focus{box-shadow:0 0 3px #fff;outline:0}.viewer-button:before{bottom:15px;left:15px;position:absolute}.viewer-fixed{position:fixed}.viewer-open{overflow:hidden}.viewer-show{display:block}.viewer-hide{display:none}.viewer-backdrop{background-color:rgba(0,0,0,.5)}.viewer-invisible{visibility:hidden}.viewer-move{cursor:move;cursor:grab}.viewer-fade{opacity:0}.viewer-in{opacity:1}.viewer-transition{transition:all .3s}@keyframes viewer-spinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.viewer-loading:after{animation:viewer-spinner 1s linear infinite;border:4px solid hsla(0,0%,100%,.1);border-left-color:hsla(0,0%,100%,.5);border-radius:50%;content:"";display:inline-block;height:40px;left:50%;margin-left:-20px;margin-top:-20px;position:absolute;top:50%;width:40px;z-index:1}@media (max-width:767px){.viewer-hide-xs-down{display:none}}@media (max-width:991px){.viewer-hide-sm-down{display:none}}@media (max-width:1199px){.viewer-hide-md-down{display:none}} \ No newline at end of file + * Date: 2026-10-01T05:04:36.958Z + */.viewer-close:before,.viewer-flip-horizontal:before,.viewer-flip-vertical:before,.viewer-fullscreen-exit:before,.viewer-fullscreen:before,.viewer-next:before,.viewer-one-to-one:before,.viewer-play:before,.viewer-prev:before,.viewer-reset:before,.viewer-rotate-left:before,.viewer-rotate-right:before,.viewer-zoom-in:before,.viewer-zoom-out:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 560 40'%3E%3Cpath fill='%23fff' d='M49.6 17.9h20.2v3.9H49.6zm123.1 2 10.9-11 2.7 2.8-8.2 8.2 8.2 8.2-2.7 2.7-10.9-10.9zm94 0-10.8-11-2.7 2.8 8.1 8.2-8.1 8.2 2.7 2.7 10.8-10.9zM212 9.3l20.1 10.6L212 30.5V9.3zm161.5 4.6-7.2 6 7.2 5.9v-4h12.4v4l7.3-5.9-7.3-6v4h-12.4v-4zm40.2 12.3 5.9 7.2 5.9-7.2h-4V13.6h4l-5.9-7.3-5.9 7.3h4v12.6h-4zm35.9-16.5h6.3v2h-4.3V16h-2V9.7Zm14 0h6.2V16h-2v-4.3h-4.2v-2Zm6.2 14V30h-6.2v-2h4.2v-4.3h2Zm-14 6.3h-6.2v-6.3h2v4.4h4.3v2Zm-438 .1v-8.3H9.6v-3.9h8.2V9.7h3.9v8.2h8.1v3.9h-8.1v8.3h-3.9zM93.6 9.7h-5.8v3.9h2V30h3.8V9.7zm16.1 0h-5.8v3.9h1.9V30h3.9V9.7zm-11.9 4.1h3.9v3.9h-3.9zm0 8.2h3.9v3.9h-3.9zm244.6-11.7 7.2 5.9-7.2 6v-3.6c-5.4-.4-7.8.8-8.7 2.8-.8 1.7-1.8 4.9 2.8 8.2-6.3-2-7.5-6.9-6-11.3 1.6-4.4 8-5 11.9-4.9v-3.1Zm147.2 13.4h6.3V30h-2v-4.3h-4.3v-2zm14 6.3v-6.3h6.2v2h-4.3V30h-1.9zm6.2-14h-6.2V9.7h1.9V14h4.3v2zm-13.9 0h-6.3v-2h4.3V9.7h2V16zm33.3 12.5 8.6-8.6-8.6-8.7 1.9-1.9 8.6 8.7 8.6-8.7 1.9 1.9-8.6 8.7 8.6 8.6-1.9 2-8.6-8.7-8.6 8.7-1.9-2zM297 10.3l-7.1 5.9 7.2 6v-3.6c5.3-.4 7.7.8 8.7 2.8.8 1.7 1.7 4.9-2.9 8.2 6.3-2 7.5-6.9 6-11.3-1.6-4.4-7.9-5-11.8-4.9v-3.1Zm-157.3-.6c2.3 0 4.4.7 6 2l2.5-3 1.9 9.2h-9.3l2.6-3.1a6.2 6.2 0 0 0-9.9 5.1c0 3.4 2.8 6.3 6.2 6.3 2.8 0 5.1-1.9 6-4.4h4c-1 4.7-5 8.3-10 8.3a10 10 0 0 1-10-10.2 10 10 0 0 1 10-10.2Z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-size:280px;color:transparent;display:block;font-size:0;height:20px;line-height:0;width:20px}.viewer-zoom-in:before{background-position:0 0;content:"Zoom In"}.viewer-zoom-out:before{background-position:-20px 0;content:"Zoom Out"}.viewer-one-to-one:before{background-position:-40px 0;content:"One to One"}.viewer-reset:before{background-position:-60px 0;content:"Reset"}.viewer-prev:before{background-position:-80px 0;content:"Previous"}.viewer-play:before{background-position:-100px 0;content:"Play"}.viewer-next:before{background-position:-120px 0;content:"Next"}.viewer-rotate-left:before{background-position:-140px 0;content:"Rotate Left"}.viewer-rotate-right:before{background-position:-160px 0;content:"Rotate Right"}.viewer-flip-horizontal:before{background-position:-180px 0;content:"Flip Horizontal"}.viewer-flip-vertical:before{background-position:-200px 0;content:"Flip Vertical"}.viewer-fullscreen:before{background-position:-220px 0;content:"Enter Full Screen"}.viewer-fullscreen-exit:before{background-position:-240px 0;content:"Exit Full Screen"}.viewer-close:before{background-position:-260px 0;content:"Close"}.viewer-container{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;bottom:0;direction:ltr;font-size:0;left:0;line-height:0;overflow:hidden;position:absolute;right:0;top:0;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.viewer-container ::-moz-selection,.viewer-container::-moz-selection{background-color:transparent}.viewer-container ::selection,.viewer-container::selection{background-color:transparent}.viewer-container:focus{outline:0}.viewer-container img{display:block;height:auto;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.viewer-canvas{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.viewer-canvas>img{height:auto;margin:15px auto;max-width:90%!important;width:auto}.viewer-magnifier{background-color:#fff;background-repeat:no-repeat;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 5px rgba(0,0,0,.5);box-sizing:border-box;display:none;left:16px;overflow:hidden;pointer-events:none;position:absolute;top:16px;z-index:2}.viewer-magnifier.viewer-show{display:block}.viewer-magnifier-image{left:0;max-width:none!important;position:absolute;top:0;-ms-transform-origin:center;transform-origin:center}.viewer-navigation{left:0;position:absolute;right:0;top:50%}.viewer-navigation>.viewer-next,.viewer-navigation>.viewer-prev{background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;height:40px;margin-top:-20px;position:absolute;width:40px}.viewer-navigation>.viewer-next:focus,.viewer-navigation>.viewer-next:hover,.viewer-navigation>.viewer-prev:focus,.viewer-navigation>.viewer-prev:hover{background-color:rgba(0,0,0,.8)}.viewer-navigation>.viewer-next:focus,.viewer-navigation>.viewer-prev:focus{box-shadow:0 0 3px #fff;outline:0}.viewer-navigation>.viewer-next:before,.viewer-navigation>.viewer-prev:before{margin:10px}.viewer-navigation>.viewer-prev{left:15px}.viewer-navigation>.viewer-next{right:15px}.viewer-navigation>.viewer-large{height:48px;margin-top:-24px;width:48px}.viewer-navigation>.viewer-large:before{margin:14px}.viewer-navigation>.viewer-small{height:32px;margin-top:-16px;width:32px}.viewer-navigation>.viewer-small:before{margin:6px}.viewer-navigation-toolbar-left>.viewer-prev{left:51px}.viewer-navigation-toolbar-right>.viewer-next{right:51px}.viewer-navigation-navbar-left>.viewer-prev{left:67px}.viewer-navigation-navbar-right>.viewer-next{right:67px}.viewer-navigation-navbar-left-small>.viewer-prev{left:57px}.viewer-navigation-navbar-right-small>.viewer-next{right:57px}.viewer-navigation-navbar-left-large>.viewer-prev{left:77px}.viewer-navigation-navbar-right-large>.viewer-next{right:77px}.viewer-navigation.viewer-navigation-toolbar-left.viewer-navigation-navbar-left>.viewer-prev{left:103px}.viewer-navigation.viewer-navigation-toolbar-left.viewer-navigation-navbar-left.viewer-navigation-navbar-left-small>.viewer-prev{left:93px}.viewer-navigation.viewer-navigation-toolbar-left.viewer-navigation-navbar-left.viewer-navigation-navbar-left-large>.viewer-prev{left:113px}.viewer-navigation.viewer-navigation-toolbar-right.viewer-navigation-navbar-right>.viewer-next{right:103px}.viewer-navigation.viewer-navigation-toolbar-right.viewer-navigation-navbar-right.viewer-navigation-navbar-right-small>.viewer-next{right:93px}.viewer-navigation.viewer-navigation-toolbar-right.viewer-navigation-navbar-right.viewer-navigation-navbar-right-large>.viewer-next{right:113px}.viewer-navbar{background-color:rgba(0,0,0,.5);bottom:0;left:0;overflow:hidden;position:absolute;right:0}.viewer-navbar.viewer-small>.viewer-list{height:40px;>li{height:40px;width:24px}}.viewer-navbar.viewer-large>.viewer-list{height:60px;>li{height:60px;width:36px}}.viewer-navbar-top{bottom:auto;top:0}.viewer-navbar-left,.viewer-navbar-right{bottom:0;top:0;width:52px;>.viewer-list{height:auto;padding:0 1px;width:50px;>li{float:none;height:30px;width:50px}>li+li{margin-left:0;margin-top:1px}}}.viewer-navbar-left.viewer-small,.viewer-navbar-right.viewer-small{width:42px;>.viewer-list{width:40px;>li{height:24px;width:40px}}}.viewer-navbar-left.viewer-large,.viewer-navbar-right.viewer-large{width:62px;>.viewer-list{width:60px;>li{height:36px;width:60px}}}.viewer-navbar-left{right:auto}.viewer-navbar-right{left:auto}.viewer-list{box-sizing:content-box;height:50px;margin:0;overflow:hidden;padding:1px 0}.viewer-list>li{color:transparent;cursor:pointer;float:left;font-size:0;height:50px;line-height:0;opacity:.5;overflow:hidden;transition:opacity .15s;width:30px}.viewer-list>li:focus,.viewer-list>li:hover{opacity:.75}.viewer-list>li:focus{outline:0}.viewer-list>li+li{margin-left:1px}.viewer-list>.viewer-loading{position:relative}.viewer-list>.viewer-loading:after{border-width:2px;height:20px;margin-left:-10px;margin-top:-10px;width:20px}.viewer-list>.viewer-active,.viewer-list>.viewer-active:focus,.viewer-list>.viewer-active:hover{opacity:1}.viewer-player{background-color:#000;bottom:0;cursor:none;display:none;right:0;z-index:1}.viewer-player,.viewer-player>img{left:0;position:absolute;top:0}.viewer-toolbar{bottom:0;left:0;position:absolute;right:0;text-align:center}.viewer-toolbar.viewer-toolbar-top{bottom:auto;top:0}.viewer-toolbar.viewer-toolbar-navbar-top{top:52px}.viewer-toolbar.viewer-toolbar-navbar-top.viewer-toolbar-navbar-small{top:42px}.viewer-toolbar.viewer-toolbar-navbar-top.viewer-toolbar-navbar-large{top:62px}.viewer-toolbar.viewer-toolbar-navbar-right{right:52px}.viewer-toolbar.viewer-toolbar-navbar-right.viewer-toolbar-navbar-small{right:42px}.viewer-toolbar.viewer-toolbar-navbar-right.viewer-toolbar-navbar-large{right:62px}.viewer-toolbar.viewer-toolbar-navbar-bottom{bottom:52px}.viewer-toolbar.viewer-toolbar-navbar-bottom.viewer-toolbar-navbar-small{bottom:42px}.viewer-toolbar.viewer-toolbar-navbar-bottom.viewer-toolbar-navbar-large{bottom:62px}.viewer-toolbar.viewer-toolbar-navbar-left{left:52px}.viewer-toolbar.viewer-toolbar-navbar-left.viewer-toolbar-navbar-small{left:42px}.viewer-toolbar.viewer-toolbar-navbar-left.viewer-toolbar-navbar-large{left:62px}.viewer-toolbar.viewer-toolbar-left,.viewer-toolbar.viewer-toolbar-right{bottom:auto;left:auto;right:0;top:50%;-ms-transform:translateY(-50%);transform:translateY(-50%);width:36px}.viewer-toolbar.viewer-toolbar-left>ul,.viewer-toolbar.viewer-toolbar-right>ul{margin:0;padding:3px 6px}.viewer-toolbar.viewer-toolbar-left>ul>li,.viewer-toolbar.viewer-toolbar-right>ul>li{float:none}.viewer-toolbar.viewer-toolbar-left>ul>li+li,.viewer-toolbar.viewer-toolbar-right>ul>li+li{margin-left:0;margin-top:1px!important}.viewer-toolbar.viewer-toolbar-left>ul>.viewer-small,.viewer-toolbar.viewer-toolbar-right>ul>.viewer-small{margin:0 3px}.viewer-toolbar.viewer-toolbar-left>ul>.viewer-large,.viewer-toolbar.viewer-toolbar-right>ul>.viewer-large{margin:0 -3px}.viewer-toolbar.viewer-toolbar-left{left:0;right:auto}.viewer-toolbar>ul{display:inline-block;margin:0 auto 5px;overflow:hidden;padding:6px 3px}.viewer-toolbar>ul>li{background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;float:left;height:24px;overflow:hidden;transition:background-color .15s;width:24px}.viewer-toolbar>ul>li:focus,.viewer-toolbar>ul>li:hover{background-color:rgba(0,0,0,.8)}.viewer-toolbar>ul>li:focus{box-shadow:0 0 3px #fff;outline:0;position:relative;z-index:1}.viewer-toolbar>ul>li:before{margin:2px}.viewer-toolbar>ul>li+li{margin-left:1px}.viewer-toolbar>ul>.viewer-small{height:18px;margin-bottom:3px;margin-top:3px;width:18px}.viewer-toolbar>ul>.viewer-small:before{margin:-1px}.viewer-toolbar>ul>.viewer-large{height:30px;margin-bottom:-3px;margin-top:-3px;width:30px}.viewer-toolbar>ul>.viewer-large:before{margin:5px}.viewer-tooltip{background-color:rgba(0,0,0,.8);border-radius:10px;color:#fff;display:none;height:20px;line-height:20px;margin-left:-25px;margin-top:-10px;text-align:center;top:50%;width:50px}.viewer-title,.viewer-tooltip{font-size:12px;left:50%;position:absolute}.viewer-title{bottom:0;color:#ccc;display:inline-block;line-height:1.2;max-width:90%;min-height:14px;opacity:.8;overflow:hidden;padding:5px;text-overflow:ellipsis;-ms-transform:translateX(-50%);transform:translateX(-50%);transition:opacity .15s;white-space:nowrap}.viewer-title.viewer-title-navbar-bottom{bottom:52px}.viewer-title.viewer-title-navbar-bottom.viewer-title-navbar-small{bottom:42px}.viewer-title.viewer-title-navbar-bottom.viewer-title-navbar-large{bottom:62px}.viewer-title.viewer-title-toolbar-bottom{bottom:42px}.viewer-title.viewer-title-toolbar-bottom.viewer-title-navbar-bottom{bottom:94px}.viewer-title.viewer-title-toolbar-bottom.viewer-title-navbar-bottom.viewer-title-navbar-small{bottom:84px}.viewer-title.viewer-title-toolbar-bottom.viewer-title-navbar-bottom.viewer-title-navbar-large{bottom:104px}.viewer-title.viewer-title-navbar-left,.viewer-title.viewer-title-navbar-right,.viewer-title.viewer-title-toolbar-left,.viewer-title.viewer-title-toolbar-right{max-width:none;text-align:center;-ms-transform:none;transform:none}.viewer-title.viewer-title-toolbar-left{left:36px;right:0}.viewer-title.viewer-title-toolbar-right{left:0;right:36px}.viewer-title.viewer-title-navbar-left{left:52px;right:0}.viewer-title.viewer-title-navbar-left.viewer-title-navbar-left-small{left:42px}.viewer-title.viewer-title-navbar-left.viewer-title-navbar-left-large{left:62px}.viewer-title.viewer-title-navbar-right{left:0;right:52px}.viewer-title.viewer-title-navbar-right.viewer-title-navbar-right-small{right:42px}.viewer-title.viewer-title-navbar-right.viewer-title-navbar-right-large{right:62px}.viewer-title.viewer-title-toolbar-left.viewer-title-navbar-right{left:36px}.viewer-title.viewer-title-toolbar-right.viewer-title-navbar-left{right:36px}.viewer-title.viewer-title-toolbar-left.viewer-title-navbar-left{left:88px}.viewer-title.viewer-title-toolbar-left.viewer-title-navbar-left.viewer-title-navbar-left-small{left:78px}.viewer-title.viewer-title-toolbar-left.viewer-title-navbar-left.viewer-title-navbar-left-large{left:98px}.viewer-title.viewer-title-toolbar-right.viewer-title-navbar-right{right:88px}.viewer-title.viewer-title-toolbar-right.viewer-title-navbar-right.viewer-title-navbar-right-small{right:78px}.viewer-title.viewer-title-toolbar-right.viewer-title-navbar-right.viewer-title-navbar-right-large{right:98px}.viewer-title:hover{opacity:1}.viewer-button{-webkit-app-region:no-drag;background-color:rgba(0,0,0,.5);border-radius:50%;cursor:pointer;height:80px;overflow:hidden;position:absolute;right:-40px;top:-40px;transition:background-color .15s;width:80px}.viewer-button:focus,.viewer-button:hover{background-color:rgba(0,0,0,.8)}.viewer-button:focus{box-shadow:0 0 3px #fff;outline:0}.viewer-button:before{bottom:15px;left:15px;position:absolute}.viewer-fixed{position:fixed}.viewer-open{overflow:hidden}.viewer-show{display:block}.viewer-hide{display:none}.viewer-backdrop{background-color:rgba(0,0,0,.5)}.viewer-invisible{visibility:hidden}.viewer-move{cursor:move;cursor:grab}.viewer-fade{opacity:0}.viewer-in{opacity:1}.viewer-transition{transition:all .3s}@keyframes viewer-spinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.viewer-loading:after{animation:viewer-spinner 1s linear infinite;border:4px solid hsla(0,0%,100%,.1);border-left-color:hsla(0,0%,100%,.5);border-radius:50%;content:"";display:inline-block;height:40px;left:50%;margin-left:-20px;margin-top:-20px;position:absolute;top:50%;width:40px;z-index:1}@media (max-width:767px){.viewer-hide-xs-down{display:none}}@media (max-width:991px){.viewer-hide-sm-down{display:none}}@media (max-width:1199px){.viewer-hide-md-down{display:none}} \ No newline at end of file diff --git a/web/lib/viewerjs/viewer.min.js b/web/lib/viewerjs/viewer.min.js index 0156b54..6b9fa49 100644 --- a/web/lib/viewerjs/viewer.min.js +++ b/web/lib/viewerjs/viewer.min.js @@ -1,10 +1,10 @@ /*! - * Viewer.js v1.11.6 + * Viewer.js v1.15.2 * https://fengyuanchen.github.io/viewerjs * * Copyright 2015-present Chen Fengyuan * Released under the MIT license * - * Date: 2023-09-17T03:16:38.052Z + * Date: 2026-10-01T05:04:39.019Z */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Viewer=e()}(this,function(){"use strict";function s(e,t){var i,n=Object.keys(e);return Object.getOwnPropertySymbols&&(i=Object.getOwnPropertySymbols(e),t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,i)),n}function P(n){for(var t=1;t=this.length||this.viewed&&t===this.index)){if(!this.isShown)return this.index=t,this.show();this.viewing&&this.viewing.abort();var e,n,o,s=this.element,a=this.options,r=this.title,l=this.canvas,h=this.items[t],c=h.querySelector("img"),u=N(c,"originalUrl"),d=c.getAttribute("alt"),m=document.createElement("img");A(a.inheritedAttributes,function(t){var e=c.getAttribute(t);null!==e&&m.setAttribute(t,e)}),m.src=u,m.alt=d,I(a.view)&&X(s,"view",a.view,{once:!0}),!1===q(s,"view",{originalImage:this.images[t],index:t,image:m})||!this.isShown||this.hiding||this.played||((u=this.items[this.index])&&(M(u,f),u.removeAttribute("aria-selected")),R(h,f),h.setAttribute("aria-selected",!0),a.focus&&h.focus(),this.image=m,this.viewed=!1,this.index=t,this.imageData={},R(m,p),a.loading&&R(l,b),l.innerHTML="",l.appendChild(m),this.renderList(),r.innerHTML="",X(s,z,e=function(){var t=i.imageData,e=Array.isArray(a.title)?a.title[1]:a.title;r.innerHTML=T(e=I(e)?e.call(i,m,t):"".concat(d," (").concat(t.naturalWidth," × ").concat(t.naturalHeight,")"))?e.replace(/&(?!amp;|quot;|#39;|lt;|gt;)/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">"):e},{once:!0}),this.viewing={abort:function(){Y(s,z,e),m.complete?i.imageRendering?i.imageRendering.abort():i.imageInitializing&&i.imageInitializing.abort():(m.src="",Y(m,y,n),i.timeout&&clearTimeout(i.timeout))}},m.complete?this.load():(X(m,y,n=function(){Y(m,x,o),i.load()},{once:!0}),X(m,x,o=function(){Y(m,y,n),i.timeout&&(clearTimeout(i.timeout),i.timeout=!1),M(m,p),a.loading&&M(i.canvas,b)},{once:!0}),this.timeout&&clearTimeout(this.timeout),this.timeout=setTimeout(function(){M(m,p),i.timeout=!1},1e3)))}return this},prev:function(){var t=this.index-1;return t<0&&(t=0Math.abs(r)&&(this.pointers={},1
',d=(i=d.querySelector(".".concat(g,"-container"))).querySelector(".".concat(g,"-title")),n=i.querySelector(".".concat(g,"-toolbar")),m=i.querySelector(".".concat(g,"-navbar")),o=i.querySelector(".".concat(g,"-button")),a=i.querySelector(".".concat(g,"-canvas")),this.parent=e,this.viewer=i,this.title=d,this.toolbar=n,this.navbar=m,this.button=o,this.canvas=a,this.footer=i.querySelector(".".concat(g,"-footer")),this.tooltipBox=i.querySelector(".".concat(g,"-tooltip")),this.player=i.querySelector(".".concat(g,"-player")),this.list=i.querySelector(".".concat(g,"-list")),i.id="".concat(g).concat(this.id),d.id="".concat(g,"Title").concat(this.id),R(d,s.title?Tt(Array.isArray(s.title)?s.title[0]:s.title):v),R(m,s.navbar?Tt(s.navbar):v),F(o,v,!s.button),s.keyboard&&o.setAttribute("tabindex",0),s.backdrop&&(R(i,"".concat(g,"-backdrop")),s.inline||"static"===s.backdrop||pt(a,ht,"hide")),T(s.className)&&s.className&&s.className.split(ct).forEach(function(t){R(i,t)}),s.toolbar?(r=document.createElement("ul"),l=S(s.toolbar),h=ut.slice(0,3),c=ut.slice(7,9),u=ut.slice(9),l||R(n,Tt(s.toolbar)),A(l?s.toolbar:ut,function(t,e){var i,n=l&&S(t),e=l?vt(e):t,o=n&&!D(t.show)?t.show:t;!o||!s.zoomable&&-1!==h.indexOf(e)||!s.rotatable&&-1!==c.indexOf(e)||!s.scalable&&-1!==u.indexOf(e)||(i=n&&!D(t.size)?t.size:t,n=n&&!D(t.click)?t.click:t,t=document.createElement("li"),s.keyboard&&t.setAttribute("tabindex",0),t.setAttribute("role","button"),R(t,"".concat(g,"-").concat(e)),I(n)||pt(t,ht,e),E(o)&&R(t,Tt(o)),-1!==["small","large"].indexOf(i)?R(t,"".concat(g,"-").concat(i)):"play"===e&&R(t,"".concat(g,"-large")),I(n)&&X(t,w,n),r.appendChild(t))}),n.appendChild(r)):R(n,v),s.rotatable||(R(d=n.querySelectorAll('li[class*="rotate"]'),p),A(d,function(t){n.appendChild(t)})),s.inline?(R(o,U),C(i,{zIndex:s.zIndexInline}),"static"===window.getComputedStyle(e).position&&C(e,{position:"relative"}),e.insertBefore(i,t.nextSibling)):(R(o,H),R(i,V),R(i,B),R(i,v),C(i,{zIndex:s.zIndex}),(m=(m=T(m=s.container)?t.ownerDocument.querySelector(m):m)||this.body).appendChild(i)),s.inline&&(this.render(),this.bind(),this.isShown=!0),this.ready=!0,I(s.ready)&&X(t,"ready",s.ready,{once:!0}),!1===q(t,"ready")?this.ready=!1:this.ready&&s.inline&&this.view(this.index))}}])&&r(t.prototype,e),i&&r(t,i),Object.defineProperty(t,"prototype",{writable:!1}),o}();return O(e.prototype,i,t,St,It,At),e}); \ No newline at end of file +((t,e)=>{"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Viewer=e()})(this,function(){function o(t,e){for(var i=0;i{if("object"!=typeof t||!t)return t;var i=t[Symbol.toPrimitive];if(void 0===i)return("string"===e?String:Number)(t);if("object"!=typeof(i=i.call(t,e||"default")))return i;throw new TypeError("@@toPrimitive must return a primitive value.")})(t,"string");return"symbol"==typeof t?t:t+""}function l(t){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var q={backdrop:!0,button:!0,navbar:!0,navigation:!1,title:!0,toolbar:!0,className:"",container:"body",filter:null,fullscreen:!0,inheritedAttributes:["crossOrigin","decoding","isMap","loading","referrerPolicy","sizes","srcset","useMap"],initialCoverage:.9,initialViewIndex:0,inline:!1,autoplay:!0,interval:5e3,keyboard:!0,focus:!0,loading:!0,loop:!0,preload:!0,minWidth:200,minHeight:100,movable:!0,magnifier:!1,rotatable:!0,rotateOnGesture:!0,rotateOnTouch:!0,scalable:!0,zoomable:!0,zoomOnTouch:!0,zoomOnGesture:!0,zoomOnWheel:!0,slideOnTouch:!0,slideOnWheel:!0,toggleOnDblclick:!0,tooltip:!0,transition:!0,zIndex:2015,zIndexInline:0,zoomRatio:.1,minZoomRatio:.01,maxZoomRatio:100,url:"src",ready:null,show:null,shown:null,hide:null,hidden:null,view:null,viewed:null,move:null,moved:null,rotate:null,rotated:null,scale:null,scaled:null,zoom:null,zoomed:null,play:null,playing:null,stop:null},t="undefined"!=typeof window&&void 0!==window.document,e=t?window:{},c=!(!t||!e.document.documentElement)&&"ontouchstart"in e.document.documentElement,i=t&&"PointerEvent"in e,x="viewer",j="move",V="rotate",H="switch",B="transform",G="zoom",K="".concat(x,"-active"),U="".concat(x,"-close"),Z="".concat(x,"-fade"),$="".concat(x,"-fixed"),_="".concat(x,"-fullscreen"),J="".concat(x,"-fullscreen-exit"),E="".concat(x,"-hide"),Q="".concat(x,"-hide-md-down"),tt="".concat(x,"-hide-sm-down"),et="".concat(x,"-hide-xs-down"),d="".concat(x,"-in"),z="".concat(x,"-invisible"),v="".concat(x,"-loading"),it="".concat(x,"-move"),nt="".concat(x,"-open"),m="".concat(x,"-show"),O="".concat(x,"-transition"),T="click",at="dblclick",ot="dragstart",st="focusin",rt="keydown",f="load",p="error",ht=i?"pointerdown":c?"touchstart":"mousedown",lt=i?"pointerenter":"mouseenter",ct=i?"pointerleave":"mouseleave",ut=i?"pointermove":c?"touchmove":"mousemove",dt=i?"pointerup pointercancel":c?"touchend touchcancel":"mouseup",u="transitionend",mt="gesturestart gesturechange gestureend",b="viewed",gt="rotated",vt="playing",ft="".concat(x,"Action"),pt=/\s+/,bt=["zoom-in","zoom-out","one-to-one","reset","prev","play","next","rotate-left","rotate-right","flip-horizontal","flip-vertical"];function k(t){return"string"==typeof t}var wt={ctrl:"ctrlKey",shift:"shiftKey",alt:"altKey",meta:"metaKey"};function yt(t,e){return!!t&&(!0===t||k(t)&&t.split("+").every(function(t){return e[wt[t.trim().toLowerCase()]]}))}var xt=Number.isNaN||e.isNaN;function D(t){return"number"==typeof t&&!xt(t)}function I(t){return void 0===t}function a(t){return"object"===l(t)&&null!==t}var Et=Object.prototype.hasOwnProperty;function M(t){if(!a(t))return!1;try{var e=t.constructor,i=e.prototype;return e&&i&&Et.call(i,"isPrototypeOf")}catch(t){return!1}}function S(t){return"function"==typeof t}function A(e,i){if(e&&S(i))if(Array.isArray(e)||D(e.length))for(var t=e.length,n=0;nMath.PI?t-=2*Math.PI:t<-Math.PI&&(t+=2*Math.PI),n.push(180*t/Math.PI)})}),n.sort(function(t,e){return Math.abs(e)-Math.abs(t)}),n[0]||0}function Yt(t,e){var i=t.pageX,t=t.pageY,n={endX:i,endY:t};return e?n:r({timeStamp:Date.now(),startX:i,startY:t},n)}var Xt,i={render:function(){this.initContainer(),this.initViewer(),this.initList(),this.renderViewer()},initBody:function(){var t=this.ownerDocument,e=t.body||t.documentElement;this.body=e,this.scrollbarWidth=window.innerWidth-t.documentElement.clientWidth,this.initialBodyPaddingRight=e.style.paddingRight,this.initialBodyComputedPaddingRight=window.getComputedStyle(e).paddingRight},initContainer:function(){this.containerData={width:window.innerWidth,height:window.innerHeight}},initViewer:function(){var t,e=this.options,i=this.parent;e.inline&&(t={width:Math.max(i.offsetWidth,e.minWidth),height:Math.max(i.offsetHeight,e.minHeight)},this.parentData=t),!this.fulled&&t||(t=this.containerData),this.viewerData=g({},t)},renderViewer:function(){this.options.inline&&!this.fulled&&N(this.viewer,this.viewerData)},initList:function(){var r=this,t=0i.right||b.clientYi.bottom||(b=D(m.scaleX)?m.scaleX:1,i=D(m.scaleY)?m.scaleY:1,o=(m.rotate||0)*Math.PI/180,n=(s=Math.cos(o))*b,a=(o=Math.sin(o))*b,o=-o*i,s=s*i,0==(u=b*i))?this.hideMagnifier():(r=(s*(h-(l=m.x+m.width/2))-o*(f-(c=m.y+m.height/2)))/u+m.width/2,h=(-a*(h-l)+n*(f-c))/u+m.height/2,c=n*(r*t-(l=m.width*t)/2)+o*(h*t-(f=m.height*t)/2),u=a*(r*t-l/2)+s*(h*t-f/2),this.magnifierSourcePoint={x:r,y:h},g.style.width="".concat(d,"px"),g.style.height="".concat(d,"px"),g.style.opacity="".concat(Math.max(0,Math.min(1,D(p)?p:1))),v.src=e,v.style.width="".concat(l,"px"),v.style.height="".concat(f,"px"),v.style.left="".concat(d/2-l/2-c,"px"),v.style.top="".concat(d/2-f/2-u,"px"),v.style.transform="rotate(".concat(m.rotate||0,"deg) scaleX(").concat(b,") scaleY(").concat(i,")"),g.removeAttribute("aria-hidden"),R(g,"viewer-show"))},hideMagnifier:function(){this.magnifier&&(y(this.magnifier,"viewer-show"),this.magnifier.setAttribute("aria-hidden",!0),this.magnifierPoint=null)},pointerdown:function(t){var e=this.options,i=this.pointers,n=this.imageData,a=t.buttons,o=t.button,s=0===Object.keys(i).length;this.pointerMoved=!1,!this.viewed||this.showing||this.viewing||this.hiding||("mousedown"===t.type||"pointerdown"===t.type&&"mouse"===t.pointerType)&&(D(a)&&1!==a||D(o)&&0!==o||t.ctrlKey)||(t.preventDefault(),t.changedTouches?A(t.changedTouches,function(t){i[t.identifier]=Yt(t)}):i[t.pointerId||0]=Yt(t),a=!!e.movable&&j,(e.zoomable&&e.zoomOnTouch||e.rotatable&&e.rotateOnTouch)&&1l.width/2?0=this.length||this.viewed&&i===n)){var t=this.actionEvent||this.viewOriginalEvent||null;if(this.actionEvent=null,this.viewOriginalEvent=t,!this.isShown)return this.index=i,this.show();this.viewing&&this.viewing.abort();var o,s,r=this.element,h=this.options,l=this.title,e=this.canvas,c=this.getItem(i),u=(c||(this.initList(i),c=this.getItem(i)),c.querySelector("img")),d=Y(u,"originalUrl"),m=u.getAttribute("alt"),g=document.createElement("img");zt(g,u,h.inheritedAttributes),g.src=d,g.alt=m,S(h.view)&&P(r,"view",h.view,{once:!0}),!1===F(r,"view",{originalImage:this.images[i],index:i,image:g,originalEvent:t})||!this.isShown||this.hiding||this.played||(this.hideMagnifier(),(d=this.getItem(n))&&(y(d,K),d.removeAttribute("aria-selected")),R(c,K),c.setAttribute("aria-selected",!0),h.focus&&c.focus(),this.image=g,this.viewed=!1,this.index=i,this.imageData={},R(g,z),h.loading&&R(e,v),e.innerHTML="",e.appendChild(g),this.renderList(),l.innerHTML="",P(r,b,o=function(){var t=a.imageData,e=Array.isArray(h.title)?h.title[1]:h.title;l.innerHTML=k(e=S(e)?e.call(a,g,t):"".concat(m," (").concat(t.naturalWidth," × ").concat(t.naturalHeight,")"))?e.replace(/&(?!amp;|quot;|#39;|lt;|gt;)/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">"):e,h.preload&&(e=(e=i+(t=i=a.length?h.loop?0Math.abs(o)&&(this.pointerMoved=!0,this.actionEvent=t,this.move(a,0))}A(i,function(t){t.startX=t.endX,t.startY=t.endY})}},isSwitchable:function(){var t=this.imageData,e=this.viewerData;return 1{function a(t){var e=1
',n=(i=n.querySelector(".".concat(x,"-container"))).querySelector(".".concat(x,"-title")),a=i.querySelector(".".concat(x,"-toolbar")),b=i.querySelector(".".concat(x,"-navbar")),s=i.querySelector(".".concat(x,"-navigation")),r=i.querySelector(".".concat(x,"-button")),h=i.querySelector(".".concat(x,"-canvas")),this.parent=e,this.viewer=i,this.title=n,this.toolbar=a,this.navbar=b,this.navigation=s,this.button=r,this.canvas=h,this.magnifier=i.querySelector(".".concat(x,"-magnifier")),this.magnifierImage=i.querySelector(".".concat(x,"-magnifier-image")),this.tooltipBox=i.querySelector(".".concat(x,"-tooltip")),this.player=i.querySelector(".".concat(x,"-player")),this.list=i.querySelector(".".concat(x,"-list")),i.id="".concat(x).concat(this.id),n.id="".concat(x,"Title").concat(this.id),R(n,o.title?W(Array.isArray(o.title)?o.title[0]:o.title):E),o.title&&n.removeAttribute("aria-hidden"),w=M(o.navbar)?o.navbar:{},l=o.navbar,c=I(w.size)?o.navbar:w.size,u=-1!==["top","right","bottom","left"].indexOf(w.position)?w.position:"bottom",R(b,(l=M(o.navbar)?!!I(w.show)||w.show:l)?W(l):E),l&&b.removeAttribute("aria-hidden"),-1!==["small","medium","large"].indexOf(c)&&R(b,"".concat(x,"-").concat(c)),this.isNavbarVertical=w="left"===u||"right"===u,R(b,"".concat(x,"-navbar-").concat(u)),l&&(b=w?"navigation":"title",R(g=w?s:n,"".concat(x,"-").concat(b,"-navbar-").concat(u)),w&&R(n,"".concat(x,"-title-navbar-").concat(u)),-1!==["small","large"].indexOf(c))&&(R(g,"".concat(x,"-").concat(b,"-navbar-").concat(w?"".concat(u,"-"):"").concat(c)),w)&&R(n,"".concat(x,"-title-navbar-").concat(u,"-").concat(c)),M(o.navigation)?A(s.querySelectorAll('[role="button"]'),function(t){var e=Y(t,ft),e=o.navigation[e],i=M(e),n=i&&!I(e.show)?e.show:e,i=i&&!I(e.size)?e.size:e;L(t,E,!n),D(n)&&R(t,W(n)),-1!==["small","large"].indexOf(i)&&R(t,"".concat(x,"-").concat(i))}):R(s,o.navigation?W(o.navigation):E),o.navigation&&s.removeAttribute("aria-hidden"),L(r,E,!o.button),o.button&&r.removeAttribute("aria-hidden"),o.keyboard&&(r.setAttribute("tabindex",0),A(s.querySelectorAll('[role="button"]'),function(t){t.setAttribute("tabindex",0)})),o.backdrop&&(R(i,"".concat(x,"-backdrop")),o.inline||"static"===o.backdrop||Dt(h,ft,"hide")),k(o.className)&&o.className&&o.className.split(pt).forEach(function(t){R(i,t)}),o.toolbar?(d=document.createElement("ul"),g=(m=M(o.toolbar))&&-1!==["top","right","bottom","left"].indexOf(o.toolbar.position)?o.toolbar.position:"bottom",v=bt.slice(0,3),f=bt.slice(7,9),p=bt.slice(9),m||R(a,W(o.toolbar)),a.removeAttribute("aria-hidden"),R(a,"".concat(x,"-toolbar-").concat(g)),R(n,"".concat(x,"-title-toolbar-").concat(g)),l&&g===u&&(R(a,"".concat(x,"-toolbar-navbar-").concat(u)),-1!==["small","large"].indexOf(c))&&R(a,"".concat(x,"-toolbar-navbar-").concat(c)),A(m?o.toolbar:bt,function(t,e){var i,n,a;"position"!==e&&(a=m&&M(t),e=m?kt(e):t,i=a&&!I(t.show)?t.show:t)&&(o.zoomable||-1===v.indexOf(e))&&(o.rotatable||-1===f.indexOf(e))&&(o.scalable||-1===p.indexOf(e))&&(n=a&&!I(t.size)?t.size:t,a=a&&!I(t.click)?t.click:t,t=document.createElement("li"),o.keyboard&&t.setAttribute("tabindex",0),t.setAttribute("role","button"),R(t,"".concat(x,"-").concat(e)),S(a)||Dt(t,ft,e),D(i)&&R(t,W(i)),-1!==["small","large"].indexOf(n)?R(t,"".concat(x,"-").concat(n)):"play"===e&&R(t,"".concat(x,"-large")),S(a)&&P(t,T,a.bind(y)),d.appendChild(t))}),a.appendChild(d),"left"!==g&&"right"!==g||R(s,"".concat(x,"-navigation-toolbar-").concat(g))):R(a,E),o.rotatable||(R(b=a.querySelectorAll('li[class*="rotate"]'),z),A(b,function(t){a.appendChild(t)})),o.inline?(R(r,_),N(i,{zIndex:o.zIndexInline}),"static"===window.getComputedStyle(e).position&&N(e,{position:"relative"}),e.insertBefore(i,t.nextSibling)):(R(r,U),R(i,$),R(i,Z),R(i,E),N(i,{zIndex:o.zIndex}),(w=(w=k(w=o.container)?this.ownerDocument.querySelector(w):w)||this.body).appendChild(i)),o.inline&&(this.render(),this.bind(),this.isShown=!0),this.ready=!0,S(o.ready)&&P(t,"ready",o.ready,{once:!0}),!1===F(t,"ready")?this.ready=!1:this.ready&&o.inline&&this.view(this.index))}}])&&o(t.prototype,e),i&&o(t,i),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,i})();return g(e.prototype,i,t,Pt,Ft,Wt),e}); \ No newline at end of file