├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── gh-pages.yml │ └── node.js.yml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── build ├── create-icons.js ├── create-iiif-thumbnails.js ├── create-translation.js ├── i18n.js └── test-translations.js ├── cypress.config.js ├── dist ├── demo.css ├── demo.js ├── favicon.ico ├── index.html ├── thumbnails │ ├── adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif │ ├── api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif │ ├── digital.library.villanova.edu-Collection-vudl-3-IIIF.avif │ ├── heritage.tudelft.nl-iiif-collection.json.avif │ ├── iiif.bodleian.ox.ac.uk-iiif-collection-top.avif │ ├── iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif │ ├── iiif.durham.ac.uk-manifests-trifle-collection-index.avif │ ├── iiif.harvardartmuseums.org-manifests-object-299843.avif │ ├── iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif │ ├── iiif.wellcomecollection.org-presentation-b20417081.avif │ ├── iiif.wellcomecollection.org-presentation-v3-collections-archives.avif │ ├── manifests.collections.yale.edu-ycba-obj-34.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif │ └── tify.rocks-manifests-iiif-cookbook-collection.json.avif ├── tify.css ├── tify.js └── translations │ ├── bg.json │ ├── de.json │ ├── eo.json │ ├── fr.json │ ├── hr.json │ ├── it.json │ ├── ja.json │ ├── nl.json │ ├── pl.json │ ├── sq.json │ ├── tr.json │ └── zh.json ├── doc ├── api.md ├── introduction.md └── user-guide.md ├── favicon.ico ├── index.html ├── package-lock.json ├── package.json ├── public ├── thumbnails │ ├── adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif │ ├── api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif │ ├── digital.library.villanova.edu-Collection-vudl-3-IIIF.avif │ ├── heritage.tudelft.nl-iiif-collection.json.avif │ ├── iiif.bodleian.ox.ac.uk-iiif-collection-top.avif │ ├── iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif │ ├── iiif.durham.ac.uk-manifests-trifle-collection-index.avif │ ├── iiif.harvardartmuseums.org-manifests-object-299843.avif │ ├── iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif │ ├── iiif.wellcomecollection.org-presentation-b20417081.avif │ ├── iiif.wellcomecollection.org-presentation-v3-collections-archives.avif │ ├── manifests.collections.yale.edu-ycba-obj-34.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif │ ├── manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif │ └── tify.rocks-manifests-iiif-cookbook-collection.json.avif └── translations │ ├── bg.json │ ├── de.json │ ├── eo.json │ ├── fr.json │ ├── hr.json │ ├── it.json │ ├── ja.json │ ├── nl.json │ ├── pl.json │ ├── sq.json │ ├── tr.json │ └── zh.json ├── src ├── App.vue ├── components │ ├── AppDropdown.vue │ ├── AppHeader.vue │ ├── AppPlayer.vue │ ├── CollectionNode.vue │ ├── IconBookOpenBlankOutline.vue │ ├── MediaFilters.vue │ ├── MetadataList.vue │ ├── PageName.vue │ ├── PageSelect.vue │ ├── PaginationButtons.vue │ ├── TocList.vue │ ├── ViewCollection.vue │ ├── ViewExport.vue │ ├── ViewHelp.vue │ ├── ViewInfo.vue │ ├── ViewMedia.vue │ ├── ViewText.vue │ ├── ViewThumbnails.vue │ └── ViewToc.vue ├── config.js ├── demo │ ├── DemoApp.vue │ ├── Instance.class.js │ ├── components │ │ ├── ColorModeSwitcher.vue │ │ ├── DemoFooter.vue │ │ ├── DemoHeader.vue │ │ ├── LanguageSwitcher.vue │ │ ├── SampleManifests.vue │ │ └── TifyLogo.vue │ ├── demo.js │ ├── imports.scss │ ├── manifests.js │ ├── modules │ │ └── filenamify.js │ └── translations │ │ ├── bg.json │ │ ├── de.json │ │ ├── eo.json │ │ ├── fr.json │ │ ├── hr.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── sq.json │ │ ├── tr.json │ │ └── zh.json ├── main.js ├── modules │ ├── filter.js │ ├── formatting.js │ ├── keyboard.js │ ├── parsing.js │ ├── promise.js │ ├── scroll.js │ └── validation.js ├── plugins │ ├── api.js │ ├── i18n.js │ ├── id.js │ └── store.js ├── strings.json └── styles │ ├── extends │ ├── button.scss │ ├── iiif-html.scss │ └── panel.scss │ ├── functions │ └── g.scss │ ├── main.scss │ ├── mixins │ ├── input.scss │ └── range.scss │ ├── sections │ ├── button-list.scss │ ├── collection.scss │ ├── dropdown.scss │ ├── error.scss │ ├── export.scss │ ├── header.scss │ ├── help.scss │ ├── icon.scss │ ├── info.scss │ ├── list.scss │ ├── loading.scss │ ├── main.scss │ ├── media.scss │ ├── page-name.scss │ ├── page-select.scss │ ├── player.scss │ ├── sr-only.scss │ ├── text.scss │ ├── thumbnails.scss │ └── toc.scss │ └── util │ ├── base.scss │ └── settings.scss ├── stylelint.config.js ├── tests ├── e2e │ ├── api.spec.js │ ├── collection.spec.js │ ├── content-state.spec.js │ ├── export.spec.js │ ├── iiif-cookbook │ │ ├── 0001-mvm-image.spec.js │ │ ├── 0002-mvm-audio.spec.js │ │ ├── 0003-mvm-video.spec.js │ │ ├── 0010-book-2-viewing-direction.spec.js │ │ ├── 0019-html-in-annotations.spec.js │ │ ├── 0021-tagging.spec.js │ │ ├── 0033-choice.spec.js │ │ ├── 0035-foldouts.spec.js │ │ ├── 0266-full-canvas-annotation.spec.js │ │ ├── 0283-missing-image.spec.js │ │ └── 0377-image-in-annotation.spec.js │ ├── iiif3.spec.js │ ├── info.spec.js │ ├── main.spec.js │ ├── media.spec.js │ ├── multi-instance.spec.js │ ├── pagination.spec.js │ ├── support │ │ └── e2e.js │ ├── text.spec.js │ ├── toc.spec.js │ └── views.spec.js ├── iiif-api │ ├── data │ │ ├── annotation-lists │ │ │ ├── 47174896.json │ │ │ └── gdz-PPN235181684_0029-00000001.json │ │ ├── annotations │ │ │ ├── default.json │ │ │ └── gdz-PPN235181684_0029-00000001.xml │ │ ├── fixtures │ │ │ └── sample.mp4 │ │ ├── images │ │ │ └── default.jpg │ │ ├── infos │ │ │ ├── 2.json │ │ │ └── default.json │ │ └── manifests │ │ │ ├── aku-pal-375.json │ │ │ ├── amherst-63cc6105-570d-407b-af8c-07fda3f8c620.json │ │ │ ├── bdrc-wio_bdr_MW22084_bdr_W22084.json │ │ │ ├── bl-vdc_00000004216E.json │ │ │ ├── bodleian-faeff7fb-f8a7-44b5-95ed-cff9a9ffd198.json │ │ │ ├── cambridge-MS-ADD-08640.json │ │ │ ├── cookbook-recipe-0019-html-in-annotations.json │ │ │ ├── cookbook-recipe-0021-tagging.json │ │ │ ├── cookbook-recipe-0266-full-canvas-annotation.json │ │ │ ├── cookbook-recipe-0283-missing-image.json │ │ │ ├── cookbook-recipe-0377-image-in-annotation.json │ │ │ ├── digitale-sammlungen-bsb00026283.json │ │ │ ├── gdz-DE_611_BF_5619_1801_1806.json │ │ │ ├── gdz-HANS_DE_7_w042081.json │ │ │ ├── gdz-PPN140716181.json │ │ │ ├── gdz-PPN235181684_0029.json │ │ │ ├── gdz-PPN857449303.json │ │ │ ├── harvard-art-museum-299843.json │ │ │ ├── invalid.json │ │ │ ├── mskgent-8210.json │ │ │ ├── not-json.json │ │ │ ├── ubl-0000000001.json │ │ │ ├── utrecht-1874-325480.json │ │ │ ├── wellcome-b18035723.json │ │ │ ├── wellcome-b19974760.json │ │ │ ├── wellcome-b19974760_1.json │ │ │ ├── wellcome-b19974760_1_0004.json │ │ │ ├── wellcome-b19974760_1_0007.json │ │ │ └── wellcome-b24738918.json │ └── server.js └── unit │ ├── App.spec.js │ ├── components │ ├── PageSelect.spec.js │ └── ViewToc.spec.js │ ├── modules │ ├── filenamify.spec.js │ ├── filter.spec.js │ └── validation.spec.js │ └── plugins │ ├── id.spec.js │ └── store.spec.js ├── vite.config.js ├── vitest.config.js └── vitest.setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{.babelrc,codecept.json,package.json}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:cypress/recommended', 9 | 'plugin:vue/vue3-recommended', 10 | '@vue/airbnb', 11 | ], 12 | globals: { 13 | ENV: true, // defined in vite.config.js 14 | }, 15 | ignorePatterns: [ 16 | 'dist', 17 | '**/data/iiif-cookbook', 18 | ], 19 | overrides: [ 20 | { 21 | files: ['*.html'], 22 | parser: '@html-eslint/parser', 23 | extends: ['plugin:@html-eslint/recommended'], 24 | }, 25 | ], 26 | parserOptions: { 27 | ecmaVersion: 'latest', 28 | }, 29 | plugins: [ 30 | '@html-eslint', 31 | 'html', 32 | ], 33 | rules: { 34 | '@html-eslint/indent': ['error', 'tab', { 35 | tagChildrenIndent: { html: 0 }, 36 | }], 37 | '@html-eslint/require-closing-tags': ['error', { 38 | selfClosingCustomPatterns: ['html'], 39 | }], 40 | 'import/prefer-default-export': 'off', 41 | 'import/no-extraneous-dependencies': ['error', { 42 | optionalDependencies: ['tests/unit/index.js'], 43 | }], 44 | indent: ['error', 'tab', { SwitchCase: 1 }], 45 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 46 | 'no-tabs': 'off', 47 | 'vue/attribute-hyphenation': ['error', 'never'], 48 | 'vue/component-name-in-template-casing': ['error', 'PascalCase', { 49 | registeredComponentsOnly: false, 50 | }], 51 | 'vue/html-indent': ['error', 'tab'], 52 | 'vue/max-attributes-per-line': ['error', { 53 | singleline: { 54 | max: 1, 55 | }, 56 | multiline: { 57 | max: 1, 58 | }, 59 | }], 60 | 'vue/max-len': ['error', 120], 61 | 'vue/no-v-html': 'off', 62 | 'vue/v-on-event-hyphenation': ['error', 'never'], 63 | 'vuejs-accessibility/heading-has-content': 'off', // disabled because of false positives 64 | }, 65 | settings: { 66 | 'import/resolver': { 67 | typescript: {}, // load /tsconfig.json to eslint, required for @iiif/parser 68 | }, 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '39 15 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to GH Pages 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | # Leave path empty for latest tag, otherwise path is PR number or branch name 9 | path: ${{ github.ref_type != 'tag' && (github.event.pull_request.number || github.ref_name) || '' }} 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Install and build 19 | run: | 20 | # Deploy each branch and pull request to tify-iiif-viewer.github.io/tify/ 21 | # Deploy latest tag to tify-iiif-viewer.github.io/tify 22 | echo BASE=/tify/${{ env.path }} >> .env 23 | echo HASHED=1 >> .env 24 | echo OUTDIR=./dist-ci/${{ env.path }} >> .env 25 | npm ci 26 | npm run build 27 | 28 | - name: Deploy 29 | uses: JamesIves/github-pages-deploy-action@v4 30 | with: 31 | branch: gh-pages 32 | clean: false 33 | folder: dist-ci 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | node_modules 3 | 4 | # Local-only files 5 | .DS_Store 6 | *.local 7 | /*.sh 8 | 9 | # Test output 10 | tests/e2e/output 11 | tests/e2e/screenshots 12 | tests/e2e/videos 13 | tests/unit/coverage 14 | 15 | # Log files 16 | *.log 17 | logs 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | pnpm-debug.log* 22 | lerna-debug.log* 23 | 24 | # Editor directories and files 25 | .idea 26 | .vscode 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/iiif-api/data/iiif-cookbook"] 2 | path = tests/iiif-api/data/iiif-cookbook 3 | url = git@github.com:IIIF/cookbook-recipes.git 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * [Tobias Schäfer](https://aspectis.net) 4 | * [Paul Pestov](https://github.com/paulpestov) 5 | * [Ingo Pfennigstorf](https://github.com/ipf) 6 | -------------------------------------------------------------------------------- /build/create-icons.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import url from 'node:url'; 3 | 4 | const rootDir = url.fileURLToPath(new URL('..', import.meta.url)); 5 | 6 | if (!fs.existsSync(`${rootDir}/node_modules/@mdi/js`)) { 7 | process.exit(); 8 | } 9 | 10 | const mdi = await import('@mdi/js'); 11 | 12 | const iconsDir = `${rootDir}/generated/icons`; 13 | 14 | fs.mkdirSync(iconsDir, { recursive: true }); 15 | 16 | Object.keys(mdi).forEach((key) => { 17 | const componentHtml = ` 18 | 19 | 24 | 25 | `; 26 | 27 | fs.writeFileSync(`${iconsDir}/Icon${key.substring(3)}.vue`, `${componentHtml.trim()}\n`); 28 | }); 29 | -------------------------------------------------------------------------------- /build/create-translation.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import readline from 'node:readline'; 3 | 4 | import chalk from 'chalk'; 5 | 6 | import { 7 | findTranslatedStrings, 8 | indention, 9 | rootDir, 10 | } from './i18n.js'; // eslint-disable-line import/extensions 11 | 12 | const rl = readline.createInterface({ 13 | input: process.stdin, 14 | output: process.stdout, 15 | }); 16 | 17 | const langCode = (await new Promise((resolve) => { 18 | rl.question('Two-letter language code (ISO 639-1): ', resolve); 19 | })).toLowerCase(); 20 | 21 | if (!langCode.match(/^[a-z]{2}$/)) { 22 | console.warn(chalk.redBright('Invalid language code')); 23 | process.exit(1); 24 | } 25 | 26 | const fileName = `${rootDir}public/translations/${langCode}.json`; 27 | 28 | if (fs.existsSync(fileName)) { 29 | console.warn(chalk.redBright(`${chalk.bold(fileName)} already exists`)); 30 | process.exit(1); 31 | } 32 | 33 | const language = await new Promise((resolve) => { 34 | rl.question('Native language name: ', resolve); 35 | }); 36 | 37 | rl.close(); 38 | 39 | const translationObject = { 40 | $language: language, 41 | }; 42 | 43 | findTranslatedStrings().forEach((item) => { 44 | translationObject[item.key] = ''; 45 | }); 46 | 47 | const json = JSON.stringify(translationObject, null, indention); 48 | 49 | fs.writeFileSync(fileName, `${json}\n`); 50 | 51 | console.log(`\nCreated ${chalk.dim('file://')}${chalk.bold(fileName)}`); 52 | -------------------------------------------------------------------------------- /build/test-translations.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { 4 | checkTranslationFiles, 5 | findTranslatedStrings, 6 | rootDir, 7 | } from './i18n.js'; // eslint-disable-line import/extensions 8 | 9 | function wrapText(text) { 10 | const words = text.split(' '); 11 | let line = ''; 12 | let output = ''; 13 | 14 | words.forEach((word) => { 15 | // Strip out invisible control characters 16 | // eslint-disable-next-line no-control-regex 17 | if ((line + word).replace(/\x1b\[[0-9;]*m/g, '').length > process.stdout.columns) { 18 | output += `${line.trimEnd()}\n`; 19 | line = ''; 20 | } 21 | 22 | line += `${word} `; 23 | }); 24 | 25 | output += line.trimEnd(); 26 | return output; 27 | } 28 | 29 | const translatedStrings = findTranslatedStrings().map((result) => result.key); 30 | 31 | if (!translatedStrings.length) { 32 | console.log('No translated strings found'); 33 | process.exit(1); 34 | } 35 | 36 | translatedStrings.unshift('$language'); 37 | 38 | const options = { 39 | addMissing: process.argv.includes('--add'), 40 | removeUnused: process.argv.includes('--remove'), 41 | sort: process.argv.includes('--sort'), 42 | }; 43 | 44 | const translationsDir = `${rootDir}public/translations`; 45 | const results = checkTranslationFiles(translationsDir, translatedStrings, options); 46 | 47 | let translationsWithIssuesCount = 0; 48 | 49 | results.forEach((result) => { 50 | console.log(`${chalk.dim('file://')}${translationsDir}/${chalk.bold(result.langCode)}.json · ${result.langName}`); 51 | 52 | ['empty', 'missing', 'unused'].forEach((type) => { 53 | const issues = result.issues.filter((issue) => issue.type === type); 54 | if (issues.length) { 55 | const label = chalk.bold(`${type.charAt(0).toUpperCase() + type.slice(1)} keys:`); 56 | console.log(wrapText(`${label} ${chalk.redBright(issues.map((issue) => issue.key).join(chalk.grey(', ')))}`)); 57 | } 58 | }); 59 | 60 | result.notes.forEach((note) => { 61 | console.log(chalk.cyanBright(note)); 62 | }); 63 | 64 | if (result.notes.length || result.issues.length) { 65 | translationsWithIssuesCount += 1; 66 | } else { 67 | console.log(chalk.greenBright('Shiny!')); 68 | } 69 | 70 | console.log(); 71 | }); 72 | 73 | console.log(`Checked ${results.length} languages, ${ 74 | translationsWithIssuesCount 75 | ? chalk.redBright(`found issues with ${translationsWithIssuesCount}.`) 76 | : chalk.greenBright('found no issues.') 77 | }`); 78 | 79 | process.exit(translationsWithIssuesCount ? 1 : 0); 80 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | import htmlvalidate from 'cypress-html-validate/plugin'; 4 | 5 | // eslint-disable-next-line import/extensions 6 | import server from './tests/iiif-api/server.js'; 7 | 8 | const iiifApiPort = 8082; 9 | 10 | server.start(iiifApiPort); 11 | 12 | export default defineConfig({ 13 | e2e: { 14 | baseUrl: 'http://localhost:4173', 15 | specPattern: 'tests/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 16 | 17 | viewportWidth: 1280, 18 | viewportHeight: 720, 19 | 20 | fixturesFolder: 'tests/e2e/fixtures', 21 | screenshotsFolder: 'tests/e2e/screenshots', 22 | videosFolder: 'tests/e2e/videos', 23 | downloadsFolder: 'tests/e2e/downloads', 24 | supportFile: 'tests/e2e/support/e2e.js', 25 | 26 | setupNodeEvents(on) { 27 | htmlvalidate.install(on); 28 | }, 29 | }, 30 | env: { 31 | iiifApiUrl: `http://0.0.0.0:${iiifApiPort}`, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/favicon.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TIFY 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /dist/thumbnails/adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif -------------------------------------------------------------------------------- /dist/thumbnails/api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif -------------------------------------------------------------------------------- /dist/thumbnails/digital.library.villanova.edu-Collection-vudl-3-IIIF.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/digital.library.villanova.edu-Collection-vudl-3-IIIF.avif -------------------------------------------------------------------------------- /dist/thumbnails/heritage.tudelft.nl-iiif-collection.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/heritage.tudelft.nl-iiif-collection.json.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.bodleian.ox.ac.uk-iiif-collection-top.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.bodleian.ox.ac.uk-iiif-collection-top.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.durham.ac.uk-manifests-trifle-collection-index.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.durham.ac.uk-manifests-trifle-collection-index.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.harvardartmuseums.org-manifests-object-299843.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.harvardartmuseums.org-manifests-object-299843.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.wellcomecollection.org-presentation-b20417081.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.wellcomecollection.org-presentation-b20417081.avif -------------------------------------------------------------------------------- /dist/thumbnails/iiif.wellcomecollection.org-presentation-v3-collections-archives.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/iiif.wellcomecollection.org-presentation-v3-collections-archives.avif -------------------------------------------------------------------------------- /dist/thumbnails/manifests.collections.yale.edu-ycba-obj-34.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/manifests.collections.yale.edu-ycba-obj-34.avif -------------------------------------------------------------------------------- /dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif -------------------------------------------------------------------------------- /dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif -------------------------------------------------------------------------------- /dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif -------------------------------------------------------------------------------- /dist/thumbnails/tify.rocks-manifests-iiif-cookbook-collection.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/dist/thumbnails/tify.rocks-manifests-iiif-cookbook-collection.json.avif -------------------------------------------------------------------------------- /dist/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Български", 3 | "$copyright": "Авторско право © 2017–2025 Университет Гьотинген / Държавна и университетска библиотека на Гьотинген", 4 | "$info": "TIFY е лек и мобилен IIIF визуализатор на документи, издаден под лиценза GNU Affero General Public License 3.0.", 5 | "About TIFY": "Относно TIFY", 6 | "Audio": "", 7 | "Brightness": "Яркост", 8 | "Close PDF list": "Затвори списъка с PDF", 9 | "Closed Captions": "", 10 | "Collapse": "Свий", 11 | "Collapse all": "Свий всички", 12 | "Collection": "Колекция", 13 | "Contents": "Съдържание", 14 | "Contrast": "Контраст", 15 | "Contributors": "Сътрудници", 16 | "Could not load child manifest": "Неуспешно зареждане на подманифест", 17 | "Current Page": "Текуща страница", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Описание", 23 | "Dismiss": "Затвори", 24 | "Document": "Документ", 25 | "Documentation": "Документация", 26 | "Exit fullscreen": "Изход от цял екран", 27 | "Expand": "Разгъни", 28 | "Expand all": "Разгъни всички", 29 | "Export [noun]": "Експортиране", 30 | "Filter collection": "Филтрирай колекцията", 31 | "Filter pages": "Филтрирай страниците", 32 | "First page": "Първа страница", 33 | "Fullscreen": "Цял екран", 34 | "Help": "Помощ", 35 | "IIIF manifest": "IIIF манифест", 36 | "IIIF manifest (collection)": "IIIF манифест (колекция)", 37 | "IIIF manifest (current document)": "IIIF манифест (текущ документ)", 38 | "Image": "", 39 | "Image filters": "Филтри за изображения", 40 | "Image missing": "", 41 | "Info": "Информация", 42 | "Last page": "Последна страница", 43 | "Layer": "", 44 | "License": "Лиценз", 45 | "Loading": "Зареждане", 46 | "Logo": "Лого", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Метаданни", 50 | "Next page": "Следваща страница", 51 | "Next section": "Следващ раздел", 52 | "No results": "Няма резултати", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Други формати", 56 | "page": "страница", 57 | "Page": "Страница", 58 | "pages": "страници", 59 | "Pages": "Страници", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF файлове за всеки елемент", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Предишна страница", 66 | "Previous section": "Предишен раздел", 67 | "Provided by": "Предоставено от", 68 | "Related Resources": "Свързани ресурси", 69 | "Renderings": "Рендери", 70 | "Report a bug": "Докладвай за проблем", 71 | "Reset": "Нулирай", 72 | "Rotate": "Завърти", 73 | "Saturation": "Наситеност", 74 | "Source code": "Изходен код", 75 | "Table of Contents": "Съдържание", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Заглавие", 79 | "Toggle annotations": "Превключване на анотации", 80 | "Toggle double-page": "Превключване на двустранен изглед", 81 | "Toggle image filters": "Превключи филтрите на изображения", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "Превключи избора на страница", 85 | "Version": "Версия", 86 | "Video": "", 87 | "View [noun]": "Изглед", 88 | "Volume": "", 89 | "Zoom in": "Увеличи", 90 | "Zoom out": "Намали" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Esperanto", 3 | "$copyright": "Kopirajtoj © 2017–2025 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen", 4 | "$info": "TIFY estas pli svelta kaj pli movebla amika IIIF-dokumentrigardilo publikigita sub la Ĝenerala Publika Permesilo 3.0 de GNU Affero.", 5 | "About TIFY": "Per TIFY", 6 | "Audio": "", 7 | "Brightness": "Helecon", 8 | "Close PDF list": "Fermu PDF-liston", 9 | "Closed Captions": "", 10 | "Collapse": "Kolapso", 11 | "Collapse all": "Kolapu ĉion", 12 | "Collection": "Kolekto", 13 | "Contents": "Enhavojn", 14 | "Contrast": "Kontrasto", 15 | "Contributors": "Kontribuanto", 16 | "Could not load child manifest": "", 17 | "Current Page": "Nuna paĝo", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Priskribo", 23 | "Dismiss": "", 24 | "Document": "Dokumento", 25 | "Documentation": "", 26 | "Exit fullscreen": "Eliru plenekranan reĝimon", 27 | "Expand": "Disfaldas", 28 | "Expand all": "Plivastigu ĉion", 29 | "Export [noun]": "Eksporti", 30 | "Filter collection": "", 31 | "Filter pages": "Filtrilaj paĝoj", 32 | "First page": "Unua paĝo", 33 | "Fullscreen": "Plena ekrana reĝimo", 34 | "Help": "Helpu", 35 | "IIIF manifest": "IIIF-Manifesto", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "Bilda filtrilo", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Lasta paĝo", 43 | "Layer": "", 44 | "License": "Permesilo", 45 | "Loading": "Ŝarĝante", 46 | "Logo": "Emblemo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadatenoj", 50 | "Next page": "Sekva paĝo", 51 | "Next section": "Sekva sekcio", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Aliaj formatoj", 56 | "page": "paĝo", 57 | "Page": "Paĝo", 58 | "pages": "paĝoj", 59 | "Pages": "Paĝoj", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF-oj por individuaj eroj", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Antaŭa paĝo", 66 | "Previous section": "Antaŭa sekcio", 67 | "Provided by": "", 68 | "Related Resources": "Rilataj fontoj", 69 | "Renderings": "Bildaj datumoj", 70 | "Report a bug": "Raportu eraron", 71 | "Reset": "Restarigi al defaŭlta", 72 | "Rotate": "Turni", 73 | "Saturation": "Saturiĝo", 74 | "Source code": "Fontkodo", 75 | "Table of Contents": "Enhavtabelo", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titolo", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Ŝaltu duoblan paĝon", 81 | "Toggle image filters": "Ŝaltu bildfiltrilojn", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "Versio", 86 | "Video": "", 87 | "View [noun]": "Vido", 88 | "Volume": "", 89 | "Zoom in": "Zomi", 90 | "Zoom out": "Malzomi" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Hrvatski", 3 | "$copyright": "Autorska prava © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY je mali i optimiziran za mobilne uređaje preglednik IIIF dokumenata, otorenog koda prema GNU Affero General Public License 3.0.", 5 | "About TIFY": "O TIFY-ju", 6 | "Audio": "", 7 | "Brightness": "Svjetlina", 8 | "Close PDF list": "Zatvori popis PDF-a", 9 | "Closed Captions": "", 10 | "Collapse": "Smanji", 11 | "Collapse all": "Smanji sve", 12 | "Collection": "Zbirka", 13 | "Contents": "Sadržaj", 14 | "Contrast": "Kontrast", 15 | "Contributors": "Suradnici", 16 | "Could not load child manifest": "", 17 | "Current Page": "Trenutna stranica", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Opis", 23 | "Dismiss": "Odbaci", 24 | "Document": "Dokument", 25 | "Documentation": "", 26 | "Exit fullscreen": "Isključi preko cijelog ekrana", 27 | "Expand": "Proširi", 28 | "Expand all": "Proširi sve", 29 | "Export [noun]": "Izvoz", 30 | "Filter collection": "Filtriraj zbirku", 31 | "Filter pages": "Filtriraj stranice", 32 | "First page": "Prva stranica", 33 | "Fullscreen": "Preko cijelog ekrana", 34 | "Help": "Pomoć", 35 | "IIIF manifest": "IIIF-Manifest", 36 | "IIIF manifest (collection)": "IIIF-Manifest (zbirka)", 37 | "IIIF manifest (current document)": "IIIF-Manifest (trenutno)", 38 | "Image": "", 39 | "Image filters": "Filteri slike", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Posljednja stranica", 43 | "Layer": "", 44 | "License": "Licenca", 45 | "Loading": "Učitavam", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metapodaci", 50 | "Next page": "Sljedeća stranica", 51 | "Next section": "Sljedeća sekcija", 52 | "No results": "Nema rezultata", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Ostali formati", 56 | "page": "stranica", 57 | "Page": "Stranica", 58 | "pages": "stranice", 59 | "Pages": "Stranice", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF za svaki element", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Prethodna stranica", 66 | "Previous section": "Prethodna sekcija", 67 | "Provided by": "", 68 | "Related Resources": "Povezani resursi", 69 | "Renderings": "Renderiranja", 70 | "Report a bug": "Prijavi grešku", 71 | "Reset": "Reset", 72 | "Rotate": "Rotiraj", 73 | "Saturation": "Zasićenje", 74 | "Source code": "Izvorni kod", 75 | "Table of Contents": "Sadržaj", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Naslov", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Dvije stranice na stranici", 81 | "Toggle image filters": "Filtri slika", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "Verzija", 86 | "Video": "", 87 | "View [noun]": "Pogled", 88 | "Volume": "", 89 | "Zoom in": "Uvećaj", 90 | "Zoom out": "Umanji" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Italiano", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Informazioni su TIFY", 6 | "Audio": "", 7 | "Brightness": "Luminosità", 8 | "Close PDF list": "Chiudi l’elenco dei PDF", 9 | "Closed Captions": "", 10 | "Collapse": "Comprimi", 11 | "Collapse all": "Comprimi tutto", 12 | "Collection": "Collezione", 13 | "Contents": "Contenuto", 14 | "Contrast": "Contrasto", 15 | "Contributors": "", 16 | "Could not load child manifest": "", 17 | "Current Page": "Pagina corrente", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Descrizione", 23 | "Dismiss": "", 24 | "Document": "Documento", 25 | "Documentation": "", 26 | "Exit fullscreen": "Esci dalla modalità a schermo intero", 27 | "Expand": "Espandi", 28 | "Expand all": "Espandi tutto", 29 | "Export [noun]": "Esporta", 30 | "Filter collection": "", 31 | "Filter pages": "", 32 | "First page": "Prima pagina", 33 | "Fullscreen": "Schermo intero", 34 | "Help": "Aiuto", 35 | "IIIF manifest": "Manifest IIIF", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "", 40 | "Image missing": "", 41 | "Info": "Informazioni", 42 | "Last page": "Ultima pagina", 43 | "Layer": "", 44 | "License": "Licenza", 45 | "Loading": "Caricamento", 46 | "Logo": "", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadati", 50 | "Next page": "Pagina successiva", 51 | "Next section": "Sezione successiva", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Altri formati", 56 | "page": "pagina", 57 | "Page": "Pagina", 58 | "pages": "pagine", 59 | "Pages": "Pagine", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF per ogni elemento", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Pagina precedente", 66 | "Previous section": "Sezione precedente", 67 | "Provided by": "", 68 | "Related Resources": "Risorse correlate", 69 | "Renderings": "Rendering", 70 | "Report a bug": "", 71 | "Reset": "Ripristina", 72 | "Rotate": "Ruota", 73 | "Saturation": "Saturazione", 74 | "Source code": "Codice sorgente", 75 | "Table of Contents": "Indice", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titolo", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Attiva/disattiva doppia pagina", 81 | "Toggle image filters": "Attiva/disattiva filtri immagine", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "", 86 | "Video": "", 87 | "View [noun]": "Vista", 88 | "Volume": "", 89 | "Zoom in": "Zoom in", 90 | "Zoom out": "Zoom out" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "日本語", 3 | "$copyright": "著作権", 4 | "$info": "情報", 5 | "About TIFY": "TIFYについて", 6 | "Audio": "", 7 | "Brightness": "明度", 8 | "Close PDF list": "PDFリストを閉じる", 9 | "Closed Captions": "", 10 | "Collapse": "折りたたむ", 11 | "Collapse all": "すべてを折りたたむ", 12 | "Collection": "コレクション", 13 | "Contents": "目次", 14 | "Contrast": "コントラスト", 15 | "Contributors": "貢献者", 16 | "Could not load child manifest": "子マニフェストを読み込めませんでした", 17 | "Current Page": "現在のページ", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "説明", 23 | "Dismiss": "閉じる", 24 | "Document": "文書", 25 | "Documentation": "ドキュメント", 26 | "Exit fullscreen": "フルスクリーン終了", 27 | "Expand": "展開", 28 | "Expand all": "すべてを展開", 29 | "Export [noun]": "エクスポート", 30 | "Filter collection": "コレクションのフィルタ", 31 | "Filter pages": "ページのフィルタ", 32 | "First page": "最初のページ", 33 | "Fullscreen": "フルスクリーン", 34 | "Help": "ヘルプ", 35 | "IIIF manifest": "IIIFマニフェスト", 36 | "IIIF manifest (collection)": "IIIFマニフェスト(コレクション)", 37 | "IIIF manifest (current document)": "IIIFマニフェスト(現在の文書)", 38 | "Image": "", 39 | "Image filters": "画像フィルタ", 40 | "Image missing": "", 41 | "Info": "情報", 42 | "Last page": "最後のページ", 43 | "Layer": "", 44 | "License": "ライセンス", 45 | "Loading": "読み込み中...", 46 | "Logo": "ロゴ", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "メタデータ", 50 | "Next page": "次のページ", 51 | "Next section": "次のセクション", 52 | "No results": "結果なし", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "その他の形式", 56 | "page": "ページ", 57 | "Page": "ページ", 58 | "pages": "ページ", 59 | "Pages": "ページ", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "各要素のPDF", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "前のページ", 66 | "Previous section": "前のセクション", 67 | "Provided by": "提供元", 68 | "Related Resources": "関連リソース", 69 | "Renderings": "レンダリング", 70 | "Report a bug": "バグを報告", 71 | "Reset": "リセット", 72 | "Rotate": "回転", 73 | "Saturation": "彩度", 74 | "Source code": "ソースコード", 75 | "Table of Contents": "目次", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "タイトル", 79 | "Toggle annotations": "注釈の表示切替", 80 | "Toggle double-page": "見開き表示の切替", 81 | "Toggle image filters": "画像フィルタの切替", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "ページ選択の切替", 85 | "Version": "バージョン", 86 | "Video": "", 87 | "View [noun]": "表示", 88 | "Volume": "", 89 | "Zoom in": "拡大", 90 | "Zoom out": "縮小" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Nederlands", 3 | "$copyright": "Copyright © 2017–2025 Universiteit Göttingen / Staats- en universiteitsbibliotheek Göttingen", 4 | "$info": "TIFY is een lichtgewicht en voor mobiel geoptimaliseerde IIIF-documentviewer, uitgebracht onder de GNU Affero General Public License 3.0.", 5 | "About TIFY": "Over TIFY", 6 | "Audio": "", 7 | "Brightness": "Helderheid", 8 | "Close PDF list": "PDF-lijst sluiten", 9 | "Closed Captions": "", 10 | "Collapse": "Inklappen", 11 | "Collapse all": "Alles inklappen", 12 | "Collection": "Collectie", 13 | "Contents": "Inhoud", 14 | "Contrast": "Contrast", 15 | "Contributors": "Bijdragers", 16 | "Could not load child manifest": "Kon het kind manifest niet laden", 17 | "Current Page": "Huidige pagina", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Beschrijving", 23 | "Dismiss": "Verbergen", 24 | "Document": "Document", 25 | "Documentation": "Documentatie", 26 | "Exit fullscreen": "Sluit de volledig scherm", 27 | "Expand": "Uitklappen", 28 | "Expand all": "Alles uitklappen", 29 | "Export [noun]": "Export", 30 | "Filter collection": "Collectie filter", 31 | "Filter pages": "Filter pagina’s", 32 | "First page": "Eerste pagina", 33 | "Fullscreen": "Volledige scherm", 34 | "Help": "Help", 35 | "IIIF manifest": "IIIF-Manifest", 36 | "IIIF manifest (collection)": "IIIF-Manifest (collectie)", 37 | "IIIF manifest (current document)": "IIIF-Manifest (document)", 38 | "Image": "", 39 | "Image filters": "Afbeeldingsfilter", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Laatste pagina", 43 | "Layer": "", 44 | "License": "Licentie", 45 | "Loading": "Word geladen", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadata", 50 | "Next page": "Volgende pagina", 51 | "Next section": "Volgende sectie", 52 | "No results": "Geen resultaten", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Andere formaten", 56 | "page": "pagina", 57 | "Page": "Pagina", 58 | "pages": "pagina’s", 59 | "Pages": "Pagina’s", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF’s voor individuele elementen", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Vorige pagina", 66 | "Previous section": "Vorige sectie", 67 | "Provided by": "Verstrekt door", 68 | "Related Resources": "Gerelateerde bronnen", 69 | "Renderings": "Afbeeldingsgegevens", 70 | "Report a bug": "Meld een fout", 71 | "Reset": "Reset", 72 | "Rotate": "Draaien", 73 | "Saturation": "Verzadiging", 74 | "Source code": "Broncode", 75 | "Table of Contents": "Inhoudsopgave", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titel", 79 | "Toggle annotations": "Annotaties wisselen", 80 | "Toggle double-page": "Dubbele pagina wisselen", 81 | "Toggle image filters": "Schakel afbeeldingsfilter in", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "Schakel paginaselectie in", 85 | "Version": "Versie", 86 | "Video": "", 87 | "View [noun]": "Weergave", 88 | "Volume": "", 89 | "Zoom in": "Vergroten", 90 | "Zoom out": "Verkleinen" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Polski", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Więcej o TIFY", 6 | "Audio": "", 7 | "Brightness": "Jasność", 8 | "Close PDF list": "Zamknij liste plików PDF", 9 | "Closed Captions": "", 10 | "Collapse": "Zwiń", 11 | "Collapse all": "Zwiń wszystko", 12 | "Collection": "Kolekcja", 13 | "Contents": "Zawartość", 14 | "Contrast": "Kontrast", 15 | "Contributors": "", 16 | "Could not load child manifest": "", 17 | "Current Page": "Obecna strona", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Opis", 23 | "Dismiss": "", 24 | "Document": "Dokument", 25 | "Documentation": "", 26 | "Exit fullscreen": "Wyjdź z trybu pełnoekranowego", 27 | "Expand": "Rozwiń", 28 | "Expand all": "Rozwiń wszystko", 29 | "Export [noun]": "Eksportuj", 30 | "Filter collection": "", 31 | "Filter pages": "", 32 | "First page": "Pierwsza Strona", 33 | "Fullscreen": "Widok pełnoekranowy", 34 | "Help": "Pomoc", 35 | "IIIF manifest": "Manifest IIIF", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "", 40 | "Image missing": "", 41 | "Info": "Informacje", 42 | "Last page": "Ostatnia strona", 43 | "Layer": "", 44 | "License": "Licencja", 45 | "Loading": "Ładowanie", 46 | "Logo": "", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadane", 50 | "Next page": "Następna strona", 51 | "Next section": "Następna sekcja", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Inne formaty", 56 | "page": "strona", 57 | "Page": "Strona", 58 | "pages": "strony", 59 | "Pages": "Strony", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "Pliki PDF dla każdego elementy", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Poprzednia strona", 66 | "Previous section": "Poprzednia sekcja", 67 | "Provided by": "", 68 | "Related Resources": "Powiązane zasoby", 69 | "Renderings": "Rendery", 70 | "Report a bug": "", 71 | "Reset": "Reset", 72 | "Rotate": "Obróć", 73 | "Saturation": "Saturacja", 74 | "Source code": "Kod źródłowy", 75 | "Table of Contents": "Spis treści", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Tytuł", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Przejdź do widoku dwóch stron", 81 | "Toggle image filters": "Włącz filtry obrazów", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "", 86 | "Video": "", 87 | "View [noun]": "Widok", 88 | "Volume": "", 89 | "Zoom in": "Przybliżenie", 90 | "Zoom out": "Oddalenie" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Türkçe", 3 | "$copyright": "Telif Hakkı © 2017–2025 Göttingen Üniversitesi / Göttingen Eyalet ve Üniversite Kütüphanesi", 4 | "$info": "TIFY, GNU Affero Genel Kamu Lisansı 3.0 altında yayımlanan, ince ve mobil uyumlu bir IIIF belge görüntüleyicisidir.", 5 | "About TIFY": "TIFY Hakkında", 6 | "Audio": "", 7 | "Brightness": "Parlaklık", 8 | "Close PDF list": "PDF listesini kapat", 9 | "Closed Captions": "", 10 | "Collapse": "Daralt", 11 | "Collapse all": "Tümünü daralt", 12 | "Collection": "Koleksiyon", 13 | "Contents": "İçindekiler", 14 | "Contrast": "Kontrast", 15 | "Contributors": "Katkıda Bulunanlar", 16 | "Could not load child manifest": "Alt manifest yüklenemedi", 17 | "Current Page": "Mevcut sayfa", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Açıklama", 23 | "Dismiss": "Kapat", 24 | "Document": "Belge", 25 | "Documentation": "Dokümantasyon", 26 | "Exit fullscreen": "Tam ekrandan çık", 27 | "Expand": "Genişlet", 28 | "Expand all": "Tümünü genişlet", 29 | "Export [noun]": "Dışa aktar", 30 | "Filter collection": "Koleksiyonu filtrele", 31 | "Filter pages": "Sayfaları filtrele", 32 | "First page": "İlk sayfa", 33 | "Fullscreen": "Tam ekran", 34 | "Help": "Yardım", 35 | "IIIF manifest": "IIIF manifestosu", 36 | "IIIF manifest (collection)": "IIIF manifestosu (koleksiyon)", 37 | "IIIF manifest (current document)": "IIIF manifestosu (mevcut belge)", 38 | "Image": "", 39 | "Image filters": "Görsel filtreleri", 40 | "Image missing": "", 41 | "Info": "Bilgi", 42 | "Last page": "Son sayfa", 43 | "Layer": "", 44 | "License": "Lisans", 45 | "Loading": "Yükleniyor", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadata", 50 | "Next page": "Sonraki sayfa", 51 | "Next section": "Sonraki bölüm", 52 | "No results": "Sonuç bulunamadı", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Diğer Formatlar", 56 | "page": "sayfa", 57 | "Page": "Sayfa", 58 | "pages": "sayfalar", 59 | "Pages": "Sayfalar", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "Her öğe için PDF'ler", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Önceki sayfa", 66 | "Previous section": "Önceki bölüm", 67 | "Provided by": "Sağlayan", 68 | "Related Resources": "İlgili Kaynaklar", 69 | "Renderings": "Görselleştirmeler", 70 | "Report a bug": "Hata bildir", 71 | "Reset": "Sıfırla", 72 | "Rotate": "Döndür", 73 | "Saturation": "Doygunluk", 74 | "Source code": "Kaynak kodu", 75 | "Table of Contents": "İçindekiler", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Başlık", 79 | "Toggle annotations": "Açıklamaları aç/kapat", 80 | "Toggle double-page": "Çift sayfa görünümünü aç/kapat", 81 | "Toggle image filters": "Görsel filtrelerini aç/kapat", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "Sayfa seçim modunu aç/kapat", 85 | "Version": "Sürüm", 86 | "Video": "", 87 | "View [noun]": "Görüntü", 88 | "Volume": "", 89 | "Zoom in": "Yakınlaştır", 90 | "Zoom out": "Uzaklaştır" 91 | } 92 | -------------------------------------------------------------------------------- /dist/translations/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "中文", 3 | "$copyright": "版权所有", 4 | "$info": "信息", 5 | "About TIFY": "关于TIFY", 6 | "Audio": "", 7 | "Brightness": "亮度", 8 | "Close PDF list": "关闭PDF列表", 9 | "Closed Captions": "", 10 | "Collapse": "收起", 11 | "Collapse all": "全部收起", 12 | "Collection": "集合", 13 | "Contents": "内容", 14 | "Contrast": "对比度", 15 | "Contributors": "贡献者", 16 | "Could not load child manifest": "无法加载子清单(manifest)", 17 | "Current Page": "当前页", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "描述", 23 | "Dismiss": "关闭", 24 | "Document": "文档", 25 | "Documentation": "文档说明", 26 | "Exit fullscreen": "退出全屏", 27 | "Expand": "展开", 28 | "Expand all": "全部展开", 29 | "Export [noun]": "导出", 30 | "Filter collection": "筛选集合", 31 | "Filter pages": "筛选页面", 32 | "First page": "首页", 33 | "Fullscreen": "全屏模式", 34 | "Help": "帮助", 35 | "IIIF manifest": "IIIF manifest", 36 | "IIIF manifest (collection)": "IIIF manifest(集合)", 37 | "IIIF manifest (current document)": "IIIF manifest(当前文档)", 38 | "Image": "", 39 | "Image filters": "图像滤镜", 40 | "Image missing": "", 41 | "Info": "信息", 42 | "Last page": "末页", 43 | "Layer": "", 44 | "License": "许可协议", 45 | "Loading": "加载中", 46 | "Logo": "徽标", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "元数据", 50 | "Next page": "下一页", 51 | "Next section": "下一章节", 52 | "No results": "无搜索结果", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "其他格式", 56 | "page": "页", 57 | "Page": "页面", 58 | "pages": "页", 59 | "Pages": "页面", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "各项目PDF文件", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "上一页", 66 | "Previous section": "上一章节", 67 | "Provided by": "提供者", 68 | "Related Resources": "相关资源", 69 | "Renderings": "其他版本", 70 | "Report a bug": "反馈问题", 71 | "Reset": "重置", 72 | "Rotate": "旋转", 73 | "Saturation": "饱和度", 74 | "Source code": "源代码", 75 | "Table of Contents": "目录", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "标题", 79 | "Toggle annotations": "显示/隐藏标注", 80 | "Toggle double-page": "单页/双页切换", 81 | "Toggle image filters": "开启/关闭图像滤镜", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "页面选择开关", 85 | "Version": "版本", 86 | "Video": "", 87 | "View [noun]": "查看", 88 | "Volume": "", 89 | "Zoom in": "放大", 90 | "Zoom out": "缩小" 91 | } 92 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | # TIFY Introduction 2 | 3 | See [readme](../README.md). 4 | -------------------------------------------------------------------------------- /doc/user-guide.md: -------------------------------------------------------------------------------- 1 | # TIFY User Guide 2 | 3 | TIFY is a slim and mobile-friendly IIIF document viewer, created with performance and usability in mind. 4 | 5 | IIIF, which stands for “International Image Interoperability Framework”, defines a set of standardized APIs for describing and delivering images along with presentational and structural metadata over the web. This allows digitized artworks, books, newspapers, manuscripts, maps, scrolls, and archival materials to be shared between institutions and repositories. Any IIIF-compliant application can consume and display those images and metadata. [Get more information about IIIF.](http://iiif.io/about/) 6 | 7 | ## Viewing Multiple Pages 8 | 9 | Any number of pages can be viewed next to each other. 10 | 11 | Open the pages view and select multiple pages by clicking on them while pressing Ctrl – or long-press on a touch screen. 12 | 13 | ## Key Bindings 14 | 15 | TIFY can be fully controlled via keyboard. 16 | 17 | ### View 18 | 19 | | Action | Key | 20 | | --- | --- | 21 | | Media | Backspace (only on small containers) | 22 | | Text | 1 (if available) | 23 | | Pages | 2 | 24 | | Contents | 3 (if available) | 25 | | Info | 4 | 26 | | Export | 5 | 27 | | Collection | 6 (if available) | 28 | | Help | 7 | 29 | | Toggle fullscreen | F | 30 | 31 | ### Turning Pages 32 | 33 | | Action | Key | 34 | | --- | --- | 35 | | Previous page | Q or , | 36 | | Next page | E or . | 37 | | First page | ⇧Q | 38 | | Last page | ⇧E | 39 | | Jump to page | X | 40 | | Toggle double-page | B | 41 | 42 | ### Scan 43 | 44 | | Action | Key | 45 | | --- | --- | 46 | | Pan | W S A D | 47 | | Zoom in | ⇧W or + | 48 | | Zoom out | ⇧S or - | 49 | | Rotate (90 degrees clockwise) | R | 50 | | Toggle filters | I | 51 | | Reset pan and zoom | 0 | 52 | | Reset rotation | + R | 53 | | Reset filters | + I | 54 | | Reset all | + 0 | 55 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TIFY 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/thumbnails/adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/adore.ugent.be-IIIF-manifests-archive.ugent.be-4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91.avif -------------------------------------------------------------------------------- /public/thumbnails/api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/api.dc.library.northwestern.edu-api-v2-collections-as=iiif.avif -------------------------------------------------------------------------------- /public/thumbnails/digital.library.villanova.edu-Collection-vudl-3-IIIF.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/digital.library.villanova.edu-Collection-vudl-3-IIIF.avif -------------------------------------------------------------------------------- /public/thumbnails/heritage.tudelft.nl-iiif-collection.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/heritage.tudelft.nl-iiif-collection.json.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.bodleian.ox.ac.uk-iiif-collection-top.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.bodleian.ox.ac.uk-iiif-collection-top.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.bodleian.ox.ac.uk-iiif-manifest-e32a277e-91e2-4a6d-8ba6-cc4bad230410.json.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.durham.ac.uk-manifests-trifle-collection-index.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.durham.ac.uk-manifests-trifle-collection-index.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.harvardartmuseums.org-manifests-object-299843.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.harvardartmuseums.org-manifests-object-299843.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.ub.uni-leipzig.de-static-collections-toplevel.json.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.wellcomecollection.org-presentation-b20417081.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.wellcomecollection.org-presentation-b20417081.avif -------------------------------------------------------------------------------- /public/thumbnails/iiif.wellcomecollection.org-presentation-v3-collections-archives.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/iiif.wellcomecollection.org-presentation-v3-collections-archives.avif -------------------------------------------------------------------------------- /public/thumbnails/manifests.collections.yale.edu-ycba-obj-34.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/manifests.collections.yale.edu-ycba-obj-34.avif -------------------------------------------------------------------------------- /public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-DE-611-HS-3216958-manifest.avif -------------------------------------------------------------------------------- /public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN1887397396-manifest-version=7a696723.avif -------------------------------------------------------------------------------- /public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/manifests.sub.uni-goettingen.de-iiif-presentation-PPN623133725-manifest.avif -------------------------------------------------------------------------------- /public/thumbnails/tify.rocks-manifests-iiif-cookbook-collection.json.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/public/thumbnails/tify.rocks-manifests-iiif-cookbook-collection.json.avif -------------------------------------------------------------------------------- /public/translations/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Esperanto", 3 | "$copyright": "Kopirajtoj © 2017–2025 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen", 4 | "$info": "TIFY estas pli svelta kaj pli movebla amika IIIF-dokumentrigardilo publikigita sub la Ĝenerala Publika Permesilo 3.0 de GNU Affero.", 5 | "About TIFY": "Per TIFY", 6 | "Audio": "", 7 | "Brightness": "Helecon", 8 | "Close PDF list": "Fermu PDF-liston", 9 | "Closed Captions": "", 10 | "Collapse": "Kolapso", 11 | "Collapse all": "Kolapu ĉion", 12 | "Collection": "Kolekto", 13 | "Contents": "Enhavojn", 14 | "Contrast": "Kontrasto", 15 | "Contributors": "Kontribuanto", 16 | "Could not load child manifest": "", 17 | "Current Page": "Nuna paĝo", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Priskribo", 23 | "Dismiss": "", 24 | "Document": "Dokumento", 25 | "Documentation": "", 26 | "Exit fullscreen": "Eliru plenekranan reĝimon", 27 | "Expand": "Disfaldas", 28 | "Expand all": "Plivastigu ĉion", 29 | "Export [noun]": "Eksporti", 30 | "Filter collection": "", 31 | "Filter pages": "Filtrilaj paĝoj", 32 | "First page": "Unua paĝo", 33 | "Fullscreen": "Plena ekrana reĝimo", 34 | "Help": "Helpu", 35 | "IIIF manifest": "IIIF-Manifesto", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "Bilda filtrilo", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Lasta paĝo", 43 | "Layer": "", 44 | "License": "Permesilo", 45 | "Loading": "Ŝarĝante", 46 | "Logo": "Emblemo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadatenoj", 50 | "Next page": "Sekva paĝo", 51 | "Next section": "Sekva sekcio", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Aliaj formatoj", 56 | "page": "paĝo", 57 | "Page": "Paĝo", 58 | "pages": "paĝoj", 59 | "Pages": "Paĝoj", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF-oj por individuaj eroj", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Antaŭa paĝo", 66 | "Previous section": "Antaŭa sekcio", 67 | "Provided by": "", 68 | "Related Resources": "Rilataj fontoj", 69 | "Renderings": "Bildaj datumoj", 70 | "Report a bug": "Raportu eraron", 71 | "Reset": "Restarigi al defaŭlta", 72 | "Rotate": "Turni", 73 | "Saturation": "Saturiĝo", 74 | "Source code": "Fontkodo", 75 | "Table of Contents": "Enhavtabelo", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titolo", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Ŝaltu duoblan paĝon", 81 | "Toggle image filters": "Ŝaltu bildfiltrilojn", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "Versio", 86 | "Video": "", 87 | "View [noun]": "Vido", 88 | "Volume": "", 89 | "Zoom in": "Zomi", 90 | "Zoom out": "Malzomi" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Hrvatski", 3 | "$copyright": "Autorska prava © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY je mali i optimiziran za mobilne uređaje preglednik IIIF dokumenata, otorenog koda prema GNU Affero General Public License 3.0.", 5 | "About TIFY": "O TIFY-ju", 6 | "Audio": "", 7 | "Brightness": "Svjetlina", 8 | "Close PDF list": "Zatvori popis PDF-a", 9 | "Closed Captions": "", 10 | "Collapse": "Smanji", 11 | "Collapse all": "Smanji sve", 12 | "Collection": "Zbirka", 13 | "Contents": "Sadržaj", 14 | "Contrast": "Kontrast", 15 | "Contributors": "Suradnici", 16 | "Could not load child manifest": "", 17 | "Current Page": "Trenutna stranica", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Opis", 23 | "Dismiss": "Odbaci", 24 | "Document": "Dokument", 25 | "Documentation": "", 26 | "Exit fullscreen": "Isključi preko cijelog ekrana", 27 | "Expand": "Proširi", 28 | "Expand all": "Proširi sve", 29 | "Export [noun]": "Izvoz", 30 | "Filter collection": "Filtriraj zbirku", 31 | "Filter pages": "Filtriraj stranice", 32 | "First page": "Prva stranica", 33 | "Fullscreen": "Preko cijelog ekrana", 34 | "Help": "Pomoć", 35 | "IIIF manifest": "IIIF-Manifest", 36 | "IIIF manifest (collection)": "IIIF-Manifest (zbirka)", 37 | "IIIF manifest (current document)": "IIIF-Manifest (trenutno)", 38 | "Image": "", 39 | "Image filters": "Filteri slike", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Posljednja stranica", 43 | "Layer": "", 44 | "License": "Licenca", 45 | "Loading": "Učitavam", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metapodaci", 50 | "Next page": "Sljedeća stranica", 51 | "Next section": "Sljedeća sekcija", 52 | "No results": "Nema rezultata", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Ostali formati", 56 | "page": "stranica", 57 | "Page": "Stranica", 58 | "pages": "stranice", 59 | "Pages": "Stranice", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF za svaki element", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Prethodna stranica", 66 | "Previous section": "Prethodna sekcija", 67 | "Provided by": "", 68 | "Related Resources": "Povezani resursi", 69 | "Renderings": "Renderiranja", 70 | "Report a bug": "Prijavi grešku", 71 | "Reset": "Reset", 72 | "Rotate": "Rotiraj", 73 | "Saturation": "Zasićenje", 74 | "Source code": "Izvorni kod", 75 | "Table of Contents": "Sadržaj", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Naslov", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Dvije stranice na stranici", 81 | "Toggle image filters": "Filtri slika", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "Verzija", 86 | "Video": "", 87 | "View [noun]": "Pogled", 88 | "Volume": "", 89 | "Zoom in": "Uvećaj", 90 | "Zoom out": "Umanji" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Italiano", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Informazioni su TIFY", 6 | "Audio": "", 7 | "Brightness": "Luminosità", 8 | "Close PDF list": "Chiudi l’elenco dei PDF", 9 | "Closed Captions": "", 10 | "Collapse": "Comprimi", 11 | "Collapse all": "Comprimi tutto", 12 | "Collection": "Collezione", 13 | "Contents": "Contenuto", 14 | "Contrast": "Contrasto", 15 | "Contributors": "", 16 | "Could not load child manifest": "", 17 | "Current Page": "Pagina corrente", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Descrizione", 23 | "Dismiss": "", 24 | "Document": "Documento", 25 | "Documentation": "", 26 | "Exit fullscreen": "Esci dalla modalità a schermo intero", 27 | "Expand": "Espandi", 28 | "Expand all": "Espandi tutto", 29 | "Export [noun]": "Esporta", 30 | "Filter collection": "", 31 | "Filter pages": "", 32 | "First page": "Prima pagina", 33 | "Fullscreen": "Schermo intero", 34 | "Help": "Aiuto", 35 | "IIIF manifest": "Manifest IIIF", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "", 40 | "Image missing": "", 41 | "Info": "Informazioni", 42 | "Last page": "Ultima pagina", 43 | "Layer": "", 44 | "License": "Licenza", 45 | "Loading": "Caricamento", 46 | "Logo": "", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadati", 50 | "Next page": "Pagina successiva", 51 | "Next section": "Sezione successiva", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Altri formati", 56 | "page": "pagina", 57 | "Page": "Pagina", 58 | "pages": "pagine", 59 | "Pages": "Pagine", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF per ogni elemento", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Pagina precedente", 66 | "Previous section": "Sezione precedente", 67 | "Provided by": "", 68 | "Related Resources": "Risorse correlate", 69 | "Renderings": "Rendering", 70 | "Report a bug": "", 71 | "Reset": "Ripristina", 72 | "Rotate": "Ruota", 73 | "Saturation": "Saturazione", 74 | "Source code": "Codice sorgente", 75 | "Table of Contents": "Indice", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titolo", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Attiva/disattiva doppia pagina", 81 | "Toggle image filters": "Attiva/disattiva filtri immagine", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "", 86 | "Video": "", 87 | "View [noun]": "Vista", 88 | "Volume": "", 89 | "Zoom in": "Zoom in", 90 | "Zoom out": "Zoom out" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "日本語", 3 | "$copyright": "著作権", 4 | "$info": "情報", 5 | "About TIFY": "TIFYについて", 6 | "Audio": "", 7 | "Brightness": "明度", 8 | "Close PDF list": "PDFリストを閉じる", 9 | "Closed Captions": "", 10 | "Collapse": "折りたたむ", 11 | "Collapse all": "すべてを折りたたむ", 12 | "Collection": "コレクション", 13 | "Contents": "目次", 14 | "Contrast": "コントラスト", 15 | "Contributors": "貢献者", 16 | "Could not load child manifest": "子マニフェストを読み込めませんでした", 17 | "Current Page": "現在のページ", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "説明", 23 | "Dismiss": "閉じる", 24 | "Document": "文書", 25 | "Documentation": "ドキュメント", 26 | "Exit fullscreen": "フルスクリーン終了", 27 | "Expand": "展開", 28 | "Expand all": "すべてを展開", 29 | "Export [noun]": "エクスポート", 30 | "Filter collection": "コレクションのフィルタ", 31 | "Filter pages": "ページのフィルタ", 32 | "First page": "最初のページ", 33 | "Fullscreen": "フルスクリーン", 34 | "Help": "ヘルプ", 35 | "IIIF manifest": "IIIFマニフェスト", 36 | "IIIF manifest (collection)": "IIIFマニフェスト(コレクション)", 37 | "IIIF manifest (current document)": "IIIFマニフェスト(現在の文書)", 38 | "Image": "", 39 | "Image filters": "画像フィルタ", 40 | "Image missing": "", 41 | "Info": "情報", 42 | "Last page": "最後のページ", 43 | "Layer": "", 44 | "License": "ライセンス", 45 | "Loading": "読み込み中...", 46 | "Logo": "ロゴ", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "メタデータ", 50 | "Next page": "次のページ", 51 | "Next section": "次のセクション", 52 | "No results": "結果なし", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "その他の形式", 56 | "page": "ページ", 57 | "Page": "ページ", 58 | "pages": "ページ", 59 | "Pages": "ページ", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "各要素のPDF", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "前のページ", 66 | "Previous section": "前のセクション", 67 | "Provided by": "提供元", 68 | "Related Resources": "関連リソース", 69 | "Renderings": "レンダリング", 70 | "Report a bug": "バグを報告", 71 | "Reset": "リセット", 72 | "Rotate": "回転", 73 | "Saturation": "彩度", 74 | "Source code": "ソースコード", 75 | "Table of Contents": "目次", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "タイトル", 79 | "Toggle annotations": "注釈の表示切替", 80 | "Toggle double-page": "見開き表示の切替", 81 | "Toggle image filters": "画像フィルタの切替", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "ページ選択の切替", 85 | "Version": "バージョン", 86 | "Video": "", 87 | "View [noun]": "表示", 88 | "Volume": "", 89 | "Zoom in": "拡大", 90 | "Zoom out": "縮小" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Nederlands", 3 | "$copyright": "Copyright © 2017–2025 Universiteit Göttingen / Staats- en universiteitsbibliotheek Göttingen", 4 | "$info": "TIFY is een lichtgewicht en voor mobiel geoptimaliseerde IIIF-documentviewer, uitgebracht onder de GNU Affero General Public License 3.0.", 5 | "About TIFY": "Over TIFY", 6 | "Audio": "", 7 | "Brightness": "Helderheid", 8 | "Close PDF list": "PDF-lijst sluiten", 9 | "Closed Captions": "", 10 | "Collapse": "Inklappen", 11 | "Collapse all": "Alles inklappen", 12 | "Collection": "Collectie", 13 | "Contents": "Inhoud", 14 | "Contrast": "Contrast", 15 | "Contributors": "Bijdragers", 16 | "Could not load child manifest": "Kon het kind manifest niet laden", 17 | "Current Page": "Huidige pagina", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Beschrijving", 23 | "Dismiss": "Verbergen", 24 | "Document": "Document", 25 | "Documentation": "Documentatie", 26 | "Exit fullscreen": "Sluit de volledig scherm", 27 | "Expand": "Uitklappen", 28 | "Expand all": "Alles uitklappen", 29 | "Export [noun]": "Export", 30 | "Filter collection": "Collectie filter", 31 | "Filter pages": "Filter pagina’s", 32 | "First page": "Eerste pagina", 33 | "Fullscreen": "Volledige scherm", 34 | "Help": "Help", 35 | "IIIF manifest": "IIIF-Manifest", 36 | "IIIF manifest (collection)": "IIIF-Manifest (collectie)", 37 | "IIIF manifest (current document)": "IIIF-Manifest (document)", 38 | "Image": "", 39 | "Image filters": "Afbeeldingsfilter", 40 | "Image missing": "", 41 | "Info": "Info", 42 | "Last page": "Laatste pagina", 43 | "Layer": "", 44 | "License": "Licentie", 45 | "Loading": "Word geladen", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadata", 50 | "Next page": "Volgende pagina", 51 | "Next section": "Volgende sectie", 52 | "No results": "Geen resultaten", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Andere formaten", 56 | "page": "pagina", 57 | "Page": "Pagina", 58 | "pages": "pagina’s", 59 | "Pages": "Pagina’s", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "PDF’s voor individuele elementen", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Vorige pagina", 66 | "Previous section": "Vorige sectie", 67 | "Provided by": "Verstrekt door", 68 | "Related Resources": "Gerelateerde bronnen", 69 | "Renderings": "Afbeeldingsgegevens", 70 | "Report a bug": "Meld een fout", 71 | "Reset": "Reset", 72 | "Rotate": "Draaien", 73 | "Saturation": "Verzadiging", 74 | "Source code": "Broncode", 75 | "Table of Contents": "Inhoudsopgave", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Titel", 79 | "Toggle annotations": "Annotaties wisselen", 80 | "Toggle double-page": "Dubbele pagina wisselen", 81 | "Toggle image filters": "Schakel afbeeldingsfilter in", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "Schakel paginaselectie in", 85 | "Version": "Versie", 86 | "Video": "", 87 | "View [noun]": "Weergave", 88 | "Volume": "", 89 | "Zoom in": "Vergroten", 90 | "Zoom out": "Verkleinen" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Polski", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Więcej o TIFY", 6 | "Audio": "", 7 | "Brightness": "Jasność", 8 | "Close PDF list": "Zamknij liste plików PDF", 9 | "Closed Captions": "", 10 | "Collapse": "Zwiń", 11 | "Collapse all": "Zwiń wszystko", 12 | "Collection": "Kolekcja", 13 | "Contents": "Zawartość", 14 | "Contrast": "Kontrast", 15 | "Contributors": "", 16 | "Could not load child manifest": "", 17 | "Current Page": "Obecna strona", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Opis", 23 | "Dismiss": "", 24 | "Document": "Dokument", 25 | "Documentation": "", 26 | "Exit fullscreen": "Wyjdź z trybu pełnoekranowego", 27 | "Expand": "Rozwiń", 28 | "Expand all": "Rozwiń wszystko", 29 | "Export [noun]": "Eksportuj", 30 | "Filter collection": "", 31 | "Filter pages": "", 32 | "First page": "Pierwsza Strona", 33 | "Fullscreen": "Widok pełnoekranowy", 34 | "Help": "Pomoc", 35 | "IIIF manifest": "Manifest IIIF", 36 | "IIIF manifest (collection)": "", 37 | "IIIF manifest (current document)": "", 38 | "Image": "", 39 | "Image filters": "", 40 | "Image missing": "", 41 | "Info": "Informacje", 42 | "Last page": "Ostatnia strona", 43 | "Layer": "", 44 | "License": "Licencja", 45 | "Loading": "Ładowanie", 46 | "Logo": "", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadane", 50 | "Next page": "Następna strona", 51 | "Next section": "Następna sekcja", 52 | "No results": "", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Inne formaty", 56 | "page": "strona", 57 | "Page": "Strona", 58 | "pages": "strony", 59 | "Pages": "Strony", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "Pliki PDF dla każdego elementy", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Poprzednia strona", 66 | "Previous section": "Poprzednia sekcja", 67 | "Provided by": "", 68 | "Related Resources": "Powiązane zasoby", 69 | "Renderings": "Rendery", 70 | "Report a bug": "", 71 | "Reset": "Reset", 72 | "Rotate": "Obróć", 73 | "Saturation": "Saturacja", 74 | "Source code": "Kod źródłowy", 75 | "Table of Contents": "Spis treści", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Tytuł", 79 | "Toggle annotations": "", 80 | "Toggle double-page": "Przejdź do widoku dwóch stron", 81 | "Toggle image filters": "Włącz filtry obrazów", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "", 85 | "Version": "", 86 | "Video": "", 87 | "View [noun]": "Widok", 88 | "Volume": "", 89 | "Zoom in": "Przybliżenie", 90 | "Zoom out": "Oddalenie" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Türkçe", 3 | "$copyright": "Telif Hakkı © 2017–2025 Göttingen Üniversitesi / Göttingen Eyalet ve Üniversite Kütüphanesi", 4 | "$info": "TIFY, GNU Affero Genel Kamu Lisansı 3.0 altında yayımlanan, ince ve mobil uyumlu bir IIIF belge görüntüleyicisidir.", 5 | "About TIFY": "TIFY Hakkında", 6 | "Audio": "", 7 | "Brightness": "Parlaklık", 8 | "Close PDF list": "PDF listesini kapat", 9 | "Closed Captions": "", 10 | "Collapse": "Daralt", 11 | "Collapse all": "Tümünü daralt", 12 | "Collection": "Koleksiyon", 13 | "Contents": "İçindekiler", 14 | "Contrast": "Kontrast", 15 | "Contributors": "Katkıda Bulunanlar", 16 | "Could not load child manifest": "Alt manifest yüklenemedi", 17 | "Current Page": "Mevcut sayfa", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "Açıklama", 23 | "Dismiss": "Kapat", 24 | "Document": "Belge", 25 | "Documentation": "Dokümantasyon", 26 | "Exit fullscreen": "Tam ekrandan çık", 27 | "Expand": "Genişlet", 28 | "Expand all": "Tümünü genişlet", 29 | "Export [noun]": "Dışa aktar", 30 | "Filter collection": "Koleksiyonu filtrele", 31 | "Filter pages": "Sayfaları filtrele", 32 | "First page": "İlk sayfa", 33 | "Fullscreen": "Tam ekran", 34 | "Help": "Yardım", 35 | "IIIF manifest": "IIIF manifestosu", 36 | "IIIF manifest (collection)": "IIIF manifestosu (koleksiyon)", 37 | "IIIF manifest (current document)": "IIIF manifestosu (mevcut belge)", 38 | "Image": "", 39 | "Image filters": "Görsel filtreleri", 40 | "Image missing": "", 41 | "Info": "Bilgi", 42 | "Last page": "Son sayfa", 43 | "Layer": "", 44 | "License": "Lisans", 45 | "Loading": "Yükleniyor", 46 | "Logo": "Logo", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "Metadata", 50 | "Next page": "Sonraki sayfa", 51 | "Next section": "Sonraki bölüm", 52 | "No results": "Sonuç bulunamadı", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "Diğer Formatlar", 56 | "page": "sayfa", 57 | "Page": "Sayfa", 58 | "pages": "sayfalar", 59 | "Pages": "Sayfalar", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "Her öğe için PDF'ler", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "Önceki sayfa", 66 | "Previous section": "Önceki bölüm", 67 | "Provided by": "Sağlayan", 68 | "Related Resources": "İlgili Kaynaklar", 69 | "Renderings": "Görselleştirmeler", 70 | "Report a bug": "Hata bildir", 71 | "Reset": "Sıfırla", 72 | "Rotate": "Döndür", 73 | "Saturation": "Doygunluk", 74 | "Source code": "Kaynak kodu", 75 | "Table of Contents": "İçindekiler", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "Başlık", 79 | "Toggle annotations": "Açıklamaları aç/kapat", 80 | "Toggle double-page": "Çift sayfa görünümünü aç/kapat", 81 | "Toggle image filters": "Görsel filtrelerini aç/kapat", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "Sayfa seçim modunu aç/kapat", 85 | "Version": "Sürüm", 86 | "Video": "", 87 | "View [noun]": "Görüntü", 88 | "Volume": "", 89 | "Zoom in": "Yakınlaştır", 90 | "Zoom out": "Uzaklaştır" 91 | } 92 | -------------------------------------------------------------------------------- /public/translations/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "中文", 3 | "$copyright": "版权所有", 4 | "$info": "信息", 5 | "About TIFY": "关于TIFY", 6 | "Audio": "", 7 | "Brightness": "亮度", 8 | "Close PDF list": "关闭PDF列表", 9 | "Closed Captions": "", 10 | "Collapse": "收起", 11 | "Collapse all": "全部收起", 12 | "Collection": "集合", 13 | "Contents": "内容", 14 | "Contrast": "对比度", 15 | "Contributors": "贡献者", 16 | "Could not load child manifest": "无法加载子清单(manifest)", 17 | "Current Page": "当前页", 18 | "Current Pages": "", 19 | "Current Section": "", 20 | "Current time": "", 21 | "Date": "", 22 | "Description": "描述", 23 | "Dismiss": "关闭", 24 | "Document": "文档", 25 | "Documentation": "文档说明", 26 | "Exit fullscreen": "退出全屏", 27 | "Expand": "展开", 28 | "Expand all": "全部展开", 29 | "Export [noun]": "导出", 30 | "Filter collection": "筛选集合", 31 | "Filter pages": "筛选页面", 32 | "First page": "首页", 33 | "Fullscreen": "全屏模式", 34 | "Help": "帮助", 35 | "IIIF manifest": "IIIF manifest", 36 | "IIIF manifest (collection)": "IIIF manifest(集合)", 37 | "IIIF manifest (current document)": "IIIF manifest(当前文档)", 38 | "Image": "", 39 | "Image filters": "图像滤镜", 40 | "Image missing": "", 41 | "Info": "信息", 42 | "Last page": "末页", 43 | "Layer": "", 44 | "License": "许可协议", 45 | "Loading": "加载中", 46 | "Logo": "徽标", 47 | "Media": "", 48 | "Media Files": "", 49 | "Metadata": "元数据", 50 | "Next page": "下一页", 51 | "Next section": "下一章节", 52 | "No results": "无搜索结果", 53 | "None": "", 54 | "Normal": "", 55 | "Other Formats": "其他格式", 56 | "page": "页", 57 | "Page": "页面", 58 | "pages": "页", 59 | "Pages": "页面", 60 | "Pause [verb]": "", 61 | "PDFs for each element": "各项目PDF文件", 62 | "Place": "", 63 | "Play [verb]": "", 64 | "Playback rate": "", 65 | "Previous page": "上一页", 66 | "Previous section": "上一章节", 67 | "Provided by": "提供者", 68 | "Related Resources": "相关资源", 69 | "Renderings": "其他版本", 70 | "Report a bug": "反馈问题", 71 | "Reset": "重置", 72 | "Rotate": "旋转", 73 | "Saturation": "饱和度", 74 | "Source code": "源代码", 75 | "Table of Contents": "目录", 76 | "Text": "", 77 | "Text not available for this page": "", 78 | "Title": "标题", 79 | "Toggle annotations": "显示/隐藏标注", 80 | "Toggle double-page": "单页/双页切换", 81 | "Toggle image filters": "开启/关闭图像滤镜", 82 | "Toggle image layer select": "", 83 | "Toggle mute": "", 84 | "Toggle page select": "页面选择开关", 85 | "Version": "版本", 86 | "Video": "", 87 | "View [noun]": "查看", 88 | "Volume": "", 89 | "Zoom in": "放大", 90 | "Zoom out": "缩小" 91 | } 92 | -------------------------------------------------------------------------------- /src/components/AppDropdown.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 100 | -------------------------------------------------------------------------------- /src/components/CollectionNode.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 111 | -------------------------------------------------------------------------------- /src/components/IconBookOpenBlankOutline.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/components/MediaFilters.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 66 | -------------------------------------------------------------------------------- /src/components/MetadataList.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 107 | -------------------------------------------------------------------------------- /src/components/PageName.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /src/components/PaginationButtons.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 78 | -------------------------------------------------------------------------------- /src/components/ViewCollection.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 70 | -------------------------------------------------------------------------------- /src/components/ViewHelp.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /src/components/ViewText.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 97 | -------------------------------------------------------------------------------- /src/components/ViewToc.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 79 | -------------------------------------------------------------------------------- /src/demo/Instance.class.js: -------------------------------------------------------------------------------- 1 | /* global Tify */ 2 | 3 | export default class Instance { 4 | constructor(options = {}) { 5 | this.colorMode = 'auto'; // TODO: Store in URL? 6 | this.hasContentState = !!(new URL(window.location)).searchParams.get('iiif-content'); 7 | this.id = options.id; 8 | this.language = options.language || 'en'; 9 | this.manifestUrl = options.manifestUrl || ''; 10 | this.sidebarOpen = false; // TODO: Store in URL? 11 | this.tify = null; 12 | } 13 | 14 | destroy() { 15 | this.manifestUrl = ''; 16 | this.tify?.destroy(); 17 | this.tify = null; 18 | 19 | Instance.updateDocumentTitle(); 20 | 21 | const url = new URL(window.location); 22 | url.searchParams.delete(`language${this.id}`); 23 | url.searchParams.delete(`manifest${this.id}`); 24 | url.searchParams.delete(`tify${this.id}`); 25 | 26 | window.history.pushState(null, '', url.toString()); 27 | } 28 | 29 | initTify(manifestUrl) { 30 | if (manifestUrl) { 31 | this.manifestUrl = manifestUrl; 32 | } 33 | 34 | this.tify?.destroy(); 35 | 36 | const url = new URL(window.location); 37 | 38 | // Update URL query if manifest was changed via form input 39 | if (!this.hasContentState 40 | && url.searchParams.get(`manifest${this.id}`) !== this.manifestUrl 41 | ) { 42 | url.searchParams.delete('iiif-content'); 43 | url.searchParams.delete(`tify${this.id}`); 44 | url.searchParams.set(`manifest${this.id}`, this.manifestUrl); 45 | window.history.pushState(null, '', url.toString()); 46 | } 47 | 48 | // TODO: Allow to add custom TIFY options like translation overrides 49 | 50 | this.tify = new Tify({ 51 | container: document.getElementById(`container${this.id}`), 52 | colorMode: this.colorMode, 53 | contentStateEnabled: this.hasContentState, 54 | language: this.language, 55 | manifestUrl: this.manifestUrl, 56 | urlQueryKey: `tify${this.id}`, 57 | }); 58 | 59 | this.tify.ready.then(() => Instance.updateDocumentTitle()); 60 | 61 | // eslint-disable-next-line no-underscore-dangle 62 | if (window.getComputedStyle(this.tify.app._container.firstChild, '::after').content !== '"wide"') { 63 | this.sidebarOpen = false; 64 | } 65 | 66 | this.hasContentState = false; 67 | 68 | // Expose latest instance for e2e tests 69 | window.tify = this.tify; 70 | } 71 | 72 | async setLanguage(code) { 73 | this.language = code; 74 | this.tify?.setLanguage(code); 75 | 76 | const url = new URL(window.location); 77 | if (code === 'en') { 78 | url.searchParams.delete(`language${this.id}`); 79 | } else { 80 | url.searchParams.set(`language${this.id}`, code); 81 | } 82 | window.history.pushState(null, '', url.toString()); 83 | } 84 | 85 | // TODO: Add test 86 | static updateDocumentTitle() { 87 | const titles = document.querySelectorAll('.tify-header-title'); 88 | document.title = `TIFY${[...titles].map((title) => ` · ${title.textContent}`).join('')}`; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/demo/components/ColorModeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | 39 | 58 | -------------------------------------------------------------------------------- /src/demo/components/DemoFooter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 42 | -------------------------------------------------------------------------------- /src/demo/components/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 57 | -------------------------------------------------------------------------------- /src/demo/components/SampleManifests.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 57 | 58 | 134 | -------------------------------------------------------------------------------- /src/demo/components/TifyLogo.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 51 | -------------------------------------------------------------------------------- /src/demo/demo.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | 3 | import DemoApp from './DemoApp.vue'; 4 | 5 | const app = createApp({ 6 | render: () => h(DemoApp), 7 | }); 8 | 9 | const translations = { en: { $language: 'English' } }; 10 | const translationModules = import.meta.glob('./translations/*.json', { eager: true }); 11 | Object.entries(translationModules).forEach(([path, module]) => { 12 | translations[path.split('/').at(-1).split('.')[0]] = module.default; 13 | }); 14 | 15 | app.config.globalProperties.$translate = (string, instance) => translations[instance.language]?.[string] 16 | || string.replace(/\s+\[.+?\]/g, ''); 17 | 18 | app.config.globalProperties.$translations = translations; 19 | 20 | app.mount('#demo'); 21 | -------------------------------------------------------------------------------- /src/demo/imports.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/functions/g'; 2 | @import '../styles/util/settings'; 3 | @import '../styles/extends/button'; 4 | -------------------------------------------------------------------------------- /src/demo/manifests.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: 'Historia Astronomiae', 4 | url: 'https://manifests.sub.uni-goettingen.de/iiif/presentation/PPN623133725/manifest', 5 | type: 'manifest', 6 | }, 7 | { 8 | title: 'Algebra Vorlesungsmanuskript', 9 | url: 'https://manifests.sub.uni-goettingen.de/iiif/presentation/DE-611-HS-3216958/manifest', 10 | type: 'manifest', 11 | }, 12 | { 13 | title: 'Commodes Manual', 14 | url: 'https://manifests.sub.uni-goettingen.de/iiif/presentation/PPN1887397396/manifest?version=7a696723', 15 | type: 'manifest', 16 | }, 17 | { 18 | title: 'Papyrus of Dioscorus of Aphrodito', 19 | url: 'https://adore.ugent.be/IIIF/manifests/archive.ugent.be:4B39C8CA-6FF9-11E1-8C42-C8A93B7C8C91', 20 | type: 'manifest', 21 | }, 22 | { 23 | title: 'The Natural Method of Healing', 24 | url: 'https://iiif.wellcomecollection.org/presentation/b20417081', 25 | type: 'manifest', 26 | }, 27 | { 28 | title: 'Self-Portrait Dedicated to Paul Gauguin', 29 | url: 'https://iiif.harvardartmuseums.org/manifests/object/299843', 30 | type: 'manifest', 31 | }, 32 | { 33 | title: 'Kalighat Paintings', 34 | url: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/e32a277e-91e2-4a6d-8ba6-cc4bad230410.json', 35 | type: 'manifest', 36 | }, 37 | { 38 | title: 'Dort or Dordrecht', 39 | url: 'https://manifests.collections.yale.edu/ycba/obj/34', 40 | type: 'manifest', 41 | }, 42 | { 43 | title: 'IIIF Cookbook', 44 | url: 'https://tify.rocks/manifests/iiif-cookbook-collection.json', 45 | type: 'collection', 46 | }, 47 | { 48 | title: 'Bodleian Libraries', 49 | url: 'https://iiif.bodleian.ox.ac.uk/iiif/collection/top', 50 | type: 'collection', 51 | }, 52 | { 53 | title: 'Durham University', 54 | url: 'https://iiif.durham.ac.uk/manifests/trifle/collection/index', 55 | type: 'collection', 56 | }, 57 | { 58 | title: 'Northwestern University Libraries', 59 | url: 'https://api.dc.library.northwestern.edu/api/v2/collections?as=iiif', 60 | type: 'collection', 61 | }, 62 | { 63 | title: 'TU Delft', 64 | url: 'https://heritage.tudelft.nl/iiif/collection.json', 65 | type: 'collection', 66 | }, 67 | { 68 | title: 'Universitätsbibliothek Leipzig', 69 | url: 'https://iiif.ub.uni-leipzig.de/static/collections/toplevel.json', 70 | type: 'collection', 71 | }, 72 | { 73 | title: 'Villanova University', 74 | url: 'https://digital.library.villanova.edu/Collection/vudl:3/IIIF', 75 | type: 'collection', 76 | }, 77 | { 78 | title: 'Wellcome Collection Archives', 79 | url: 'https://iiif.wellcomecollection.org/presentation/v3/collections/archives', 80 | type: 'collection', 81 | }, 82 | ]; 83 | -------------------------------------------------------------------------------- /src/demo/modules/filenamify.js: -------------------------------------------------------------------------------- 1 | export function filenamifyUrl(url) { 2 | return url 3 | .replace(/^https?:\/\//, '') 4 | .replace(/[\\/:*?"<>|]/g, '-'); 5 | } 6 | -------------------------------------------------------------------------------- /src/demo/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Български", 3 | "Add instance": "Добавяне на инстанция", 4 | "auto": "автоматично", 5 | "Close sidebar": "Затвори страничната лента", 6 | "Collection": "Колекция", 7 | "Color mode": "Цветови режим", 8 | "dark": "тъмен", 9 | "IIIF manifest URL": "IIIF manifest URL", 10 | "Language": "Език", 11 | "light [adjective]": "светъл", 12 | "Load": "Зареждане", 13 | "Remove instance": "Премахни инстанция", 14 | "Sample IIIF manifests": "Примерни IIIF манифести" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Deutsch", 3 | "Add instance": "Neue Instanz", 4 | "auto": "automatisch", 5 | "Close sidebar": "", 6 | "Collection": "Sammlung", 7 | "Color mode": "Farbmodus", 8 | "dark": "dunkel", 9 | "IIIF manifest URL": "IIIF-Manifest-URL", 10 | "Language": "Sprache", 11 | "light [adjective]": "hell", 12 | "Load": "Laden", 13 | "Remove instance": "Entferne Instanz", 14 | "Sample IIIF manifests": "Beispiel-IIIF-Manifeste" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Esperanto", 3 | "Add instance": "Aldoni instancon", 4 | "auto": "aŭtomate", 5 | "Close sidebar": "Fermi flankan panelon", 6 | "Collection": "Kolekto", 7 | "Color mode": "Kolora reĝimo", 8 | "dark": "malhela", 9 | "IIIF manifest URL": "IIIF-manifesta URL", 10 | "Language": "Lingvo", 11 | "light [adjective]": "hela", 12 | "Load": "Ŝargi", 13 | "Remove instance": "Forigi instancon", 14 | "Sample IIIF manifests": "Ekzemplaj IIIF-manifestoj" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Français", 3 | "Add instance": "Nouvelle instance", 4 | "auto": "automatique", 5 | "Close sidebar": "Fermer la barre latérale", 6 | "Collection": "Collection", 7 | "Color mode": "Mode couleur", 8 | "dark": "sombre", 9 | "IIIF manifest URL": "URL du manifeste IIIF", 10 | "Language": "Langue", 11 | "light [adjective]": "clair", 12 | "Load": "Charger", 13 | "Remove instance": "Supprimer l’instance", 14 | "Sample IIIF manifests": "Exemples de manifestes IIIF" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Hrvatski", 3 | "Add instance": "Dodaj instancu", 4 | "auto": "automatski", 5 | "Close sidebar": "Zatvori bočnu traku", 6 | "Collection": "Zbirka", 7 | "Color mode": "Način boja", 8 | "dark": "tamno", 9 | "IIIF manifest URL": "IIIF manifest URL", 10 | "Language": "Jezik", 11 | "light [adjective]": "svijetlo", 12 | "Load": "Učitaj", 13 | "Remove instance": "Ukloni instancu", 14 | "Sample IIIF manifests": "Primjeri IIIF manifesta" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Italiano", 3 | "Add instance": "Aggiungi istanza", 4 | "auto": "automatico", 5 | "Close sidebar": "Chiudi barra laterale", 6 | "Collection": "Collezione", 7 | "Color mode": "Modalità colore", 8 | "dark": "scuro", 9 | "IIIF manifest URL": "URL del manifesto IIIF", 10 | "Language": "Lingua", 11 | "light [adjective]": "chiaro", 12 | "Load": "Carica", 13 | "Remove instance": "Rimuovi istanza", 14 | "Sample IIIF manifests": "Manifesti IIIF di esempio" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "日本語", 3 | "Add instance": "インスタンスを追加", 4 | "auto": "自動", 5 | "Close sidebar": "サイドバーを閉じる", 6 | "Collection": "コレクション", 7 | "Color mode": "カラーモード", 8 | "dark": "ダーク", 9 | "IIIF manifest URL": "IIIF マニフェスト URL", 10 | "Language": "言語", 11 | "light [adjective]": "ライト", 12 | "Load": "読み込み", 13 | "Remove instance": "インスタンスを削除", 14 | "Sample IIIF manifests": "サンプル IIIF マニフェスト" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Nederlands", 3 | "Add instance": "Instantie toevoegen", 4 | "auto": "automatisch", 5 | "Close sidebar": "Zijbalk sluiten", 6 | "Collection": "Collectie", 7 | "Color mode": "Kleurmodus", 8 | "dark": "donker", 9 | "IIIF manifest URL": "IIIF-manifest-URL", 10 | "Language": "Taal", 11 | "light [adjective]": "licht", 12 | "Load": "Laden", 13 | "Remove instance": "Instantie verwijderen", 14 | "Sample IIIF manifests": "Voorbeeld IIIF-manifesten" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Polski", 3 | "Add instance": "Dodaj instancję", 4 | "auto": "automatycznie", 5 | "Close sidebar": "Zamknij pasek boczny", 6 | "Collection": "Kolekcja", 7 | "Color mode": "Tryb kolorów", 8 | "dark": "ciemny", 9 | "IIIF manifest URL": "URL manifestu IIIF", 10 | "Language": "Język", 11 | "light [adjective]": "jasny", 12 | "Load": "Załaduj", 13 | "Remove instance": "Usuń instancję", 14 | "Sample IIIF manifests": "Przykładowe manifesty IIIF" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/sq.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Shqip", 3 | "Add instance": "Shto instancë", 4 | "auto": "automatikisht", 5 | "Close sidebar": "Mbyll shiritin anësor", 6 | "Collection": "Koleksion", 7 | "Color mode": "Mënyra e ngjyrave", 8 | "dark": "i errët", 9 | "IIIF manifest URL": "URL e manifestit IIIF", 10 | "Language": "Gjuha", 11 | "light [adjective]": "i hapur", 12 | "Load": "Ngarko", 13 | "Remove instance": "Hiq instancën", 14 | "Sample IIIF manifests": "Shembuj të manifesteve IIIF" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Türkçe", 3 | "Add instance": "Yeni örnek", 4 | "auto": "otomatik", 5 | "Close sidebar": "Kenar çubuğunu kapat", 6 | "Collection": "Koleksiyon", 7 | "Color mode": "Renk modu", 8 | "dark": "koyu", 9 | "IIIF manifest URL": "IIIF manifest URL'si", 10 | "Language": "Dil", 11 | "light [adjective]": "açık", 12 | "Load": "Yükle", 13 | "Remove instance": "Örneği kaldır", 14 | "Sample IIIF manifests": "Örnek IIIF manifestleri" 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/translations/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "中文", 3 | "Add instance": "新增实例", 4 | "auto": "自动", 5 | "Close sidebar": "关闭侧边栏", 6 | "Collection": "集合", 7 | "Color mode": "颜色模式", 8 | "dark": "深色", 9 | "IIIF manifest URL": "IIIF 清单 URL", 10 | "Language": "语言", 11 | "light [adjective]": "浅色", 12 | "Load": "加载", 13 | "Remove instance": "移除实例", 14 | "Sample IIIF manifests": "IIIF 示例清单" 15 | } 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | 3 | import App from './App.vue'; 4 | 5 | import defaultOptions from './config'; 6 | 7 | import api from './plugins/api'; 8 | import i18n from './plugins/i18n'; 9 | import id from './plugins/id'; 10 | import store from './plugins/store'; 11 | 12 | window.Tify = function Tify(userOptions = {}) { 13 | this.options = { 14 | // Create independent deep clone 15 | ...JSON.parse(JSON.stringify(defaultOptions)), 16 | ...userOptions, 17 | }; 18 | 19 | if (!this.options.translationsDirUrl) { 20 | if (import.meta.env.DEV) { 21 | this.options.translationsDirUrl = 'translations'; 22 | } else { 23 | try { 24 | const { url } = import.meta; 25 | this.options.translationsDirUrl = `${url.slice(0, url.lastIndexOf('/'))}/translations`; 26 | } catch { 27 | // Nothing to do here 28 | } 29 | } 30 | } 31 | 32 | let readyPromise = null; 33 | this.ready = new Promise((resolve, reject) => { 34 | readyPromise = { resolve, reject }; 35 | }); 36 | 37 | const instance = this; 38 | this.app = createApp({ 39 | render: () => h(App, { readyPromise }), 40 | }) 41 | .use(api, { instance }) 42 | .use(i18n) 43 | .use(id) 44 | .use(store, { options: this.options }); 45 | 46 | // TODO: Add test 47 | let mounted = false; 48 | this.mount = (container) => { 49 | if (mounted) { 50 | throw new Error('TIFY is already mounted'); 51 | } 52 | 53 | const containerEl = typeof container === 'string' 54 | ? document.querySelector(container) 55 | : container; 56 | 57 | if (!containerEl) { 58 | throw new Error('Container element not found'); 59 | } 60 | 61 | this.app.mount(containerEl); 62 | 63 | mounted = true; 64 | }; 65 | 66 | // TODO: Add test 67 | this.destroy = () => { 68 | this.app.unmount(); 69 | }; 70 | 71 | if (this.options.container) { 72 | this.mount(this.options.container); 73 | } 74 | }; 75 | 76 | export default window.Tify; 77 | -------------------------------------------------------------------------------- /src/modules/filter.js: -------------------------------------------------------------------------------- 1 | import striptags from 'striptags'; 2 | 3 | export function filterHtml(html) { 4 | // See http://iiif.io/api/presentation/2.1/#html-markup-in-property-values 5 | const allowedTags = ['a', 'b', 'br', 'i', 'img', 'p', 'span']; 6 | const allowedAttributes = { a: ['href'], img: ['alt', 'src'] }; 7 | 8 | // TODO: striptags removes '<' and '>' inside attribute values 9 | let filteredHtml = striptags(html, allowedTags); 10 | 11 | // Iterate over all opening (including self-closing) HTML tags 12 | const htmlTagsRegex = /<(\w+)((\s+.+?(\s*=\s*(?:".*?"|'.*?'|.*?|[\^'">\s]+))?)+\s*|\s*)>/g; 13 | filteredHtml = filteredHtml.replace(htmlTagsRegex, (match, tag, attributes) => { 14 | if (!attributes) { 15 | return `<${tag}>`; 16 | } 17 | 18 | // Iterate over all attributes and keep only allowed ones 19 | const attributesRegex = /(?:([^\s"'=]+)(?:=(?:"(.*?)"|'(.*?)'|([^\s>]+)))?)/g; 20 | const keptAttributes = []; 21 | attributes.replace(attributesRegex, (tuple, key) => { 22 | if (allowedAttributes[tag]?.includes(key)) { 23 | keptAttributes.push(tuple); 24 | } 25 | }); 26 | 27 | return keptAttributes.length > 0 ? `<${tag} ${keptAttributes.join(' ')}>` : `<${tag}>`; 28 | }); 29 | 30 | return filteredHtml; 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/formatting.js: -------------------------------------------------------------------------------- 1 | export function formatDate(string, language) { 2 | try { 3 | return new Date(string).toLocaleDateString(language, { 4 | month: 'long', 5 | day: 'numeric', 6 | year: 'numeric', 7 | }); 8 | } catch { 9 | return string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/keyboard.js: -------------------------------------------------------------------------------- 1 | export function preventEvent(event) { 2 | if (event.altKey || event.ctrlKey || event.metaKey) { 3 | return true; 4 | } 5 | 6 | if (['INPUT', 'SELECT', 'TEXTAREA'].includes(event.target.nodeName) && event.target.type !== 'range') { 7 | return true; 8 | } 9 | 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/parsing.js: -------------------------------------------------------------------------------- 1 | export function parseCoordinatesString(coordinatesString) { 2 | return coordinatesString 3 | ?.split('xywh=')[1] 4 | ?.split(',') 5 | .map((number) => parseFloat(number)); 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/promise.js: -------------------------------------------------------------------------------- 1 | export function createPromise() { 2 | let resolveFunction; 3 | let rejectFunction; 4 | 5 | const promise = new Promise((resolve, reject) => { 6 | resolveFunction = resolve; 7 | rejectFunction = reject; 8 | }); 9 | 10 | promise.resolve = resolveFunction; 11 | promise.reject = rejectFunction; 12 | 13 | return promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/scroll.js: -------------------------------------------------------------------------------- 1 | export function scrollTo(element, to, animationDuration = 120) { 2 | const el = element; 3 | const duration = animationDuration === true ? 120 : animationDuration; 4 | 5 | if (!duration || duration < 0) { 6 | el.scrollTop = to; 7 | return; 8 | } 9 | 10 | const difference = to - element.scrollTop; 11 | const perTick = difference / duration / 0.1; 12 | 13 | setTimeout(() => { 14 | el.scrollTop += perTick; 15 | if (el.scrollTop === to) { 16 | return; 17 | } 18 | 19 | scrollTo(el, to, duration - 10); 20 | }, 10); 21 | } 22 | 23 | export function updateScrollPos(selector, ancestorElement, animated = true) { 24 | const elements = ancestorElement.querySelectorAll(selector); 25 | if (!elements.length) { 26 | return; 27 | } 28 | 29 | let topCurrentElement = elements[0]; 30 | const bottomCurrentElement = elements[elements.length - 1]; 31 | Array.prototype.forEach.call(elements, (element) => { 32 | if (element.dataset.level >= topCurrentElement.dataset.level) { 33 | topCurrentElement = element; 34 | } 35 | }); 36 | 37 | const listRect = ancestorElement.getBoundingClientRect(); 38 | const topCurrentElementRect = topCurrentElement.getBoundingClientRect(); 39 | const bottomCurrentElementRect = bottomCurrentElement.getBoundingClientRect(); 40 | 41 | if (topCurrentElementRect.top < listRect.top) { 42 | const targetPos = topCurrentElementRect.top - listRect.top + ancestorElement.scrollTop; 43 | scrollTo(ancestorElement, targetPos - 50, animated); 44 | } else if (bottomCurrentElementRect.bottom > listRect.bottom) { 45 | const targetPos = bottomCurrentElementRect.bottom - listRect.bottom + ancestorElement.scrollTop; 46 | scrollTo(ancestorElement, targetPos + 50, animated); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/validation.js: -------------------------------------------------------------------------------- 1 | export function isValidPagesArray(pages, pageCount) { 2 | if (!(pages instanceof Array) || !pageCount) { 3 | return false; 4 | } 5 | 6 | // Check for duplicates 7 | if (new Set(pages).size !== pages.length) { 8 | return false; 9 | } 10 | 11 | // Check if all pages exist 12 | for (let i = 0, len = pages.length; i < len; i += 1) { 13 | if (!Number.isInteger(pages[i]) 14 | // Are there pages out of order? 15 | || (i > 0 && pages[i] > 0 && pages[i] <= pages[i - 1]) 16 | // Are there pages out of range? 0 and -1 serve as placeholder values. 17 | || pages[i] < -1 || pages[i] > pageCount 18 | ) return false; 19 | } 20 | 21 | return true; 22 | } 23 | 24 | export function isValidUrl(string, allowedProtocols = ['https:', 'http:']) { 25 | let url; 26 | 27 | try { 28 | url = new URL(string); 29 | } catch { 30 | return false; 31 | } 32 | 33 | return allowedProtocols.includes(url.protocol); 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/api.js: -------------------------------------------------------------------------------- 1 | function Api(instance) { 2 | return { 3 | expose(method, name) { 4 | // eslint-disable-next-line no-param-reassign 5 | instance[name || method.name.replace('bound ', '')] = method; 6 | }, 7 | }; 8 | } 9 | 10 | export default { 11 | install: (app, options) => { 12 | // eslint-disable-next-line no-param-reassign 13 | app.config.globalProperties.$api = new Api(options.instance); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | import strings from '../strings.json'; 4 | 5 | export default { 6 | install: (app) => { 7 | const translation = ref(null); 8 | 9 | // eslint-disable-next-line no-param-reassign 10 | app.config.globalProperties.$translate = (string) => { 11 | const { language } = app.config.globalProperties.$store.options; 12 | const override = app.config.globalProperties.$store.options.translations?.[language]?.[string]; 13 | if (override) { 14 | return override; 15 | } 16 | 17 | if (translation.value?.[string]) { 18 | return translation.value[string]; 19 | } 20 | 21 | if (import.meta.env.DEV && translation.value) { 22 | // eslint-disable-next-line no-console 23 | console.warn(`Missing translation for "${string}"`); 24 | } 25 | 26 | return strings[string] || string.replace(/\s*\[.+?\]/g, ''); 27 | }; 28 | 29 | // NOTE: translationObject contains any number of key-value pairs, where 30 | // the key is the string in the default language (usually English), the 31 | // value is the translated string, e.g. { key: 'Schlüssel' } 32 | // eslint-disable-next-line no-param-reassign 33 | app.config.globalProperties.$translate.setTranslation = (translationObject) => { 34 | translation.value = translationObject; 35 | }; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/plugins/id.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install: (app) => { 3 | // Crypto is only available in newer browsers and with HTTPS 4 | const appId = crypto?.randomUUID 5 | ? crypto.randomUUID() 6 | : Math.random().toString().slice(2); 7 | 8 | // eslint-disable-next-line no-param-reassign 9 | app.config.globalProperties.$getId = (label) => `${appId}-${label}`; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$info": "TIFY is a slim and mobile-friendly IIIF document viewer, released under the GNU Affero General Public License 3.0.", 3 | "$copyright": "Copyright © 2017–2025 Göttingen University / Göttingen State and University Library", 4 | "$n/a": "‒" 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/extends/button.scss: -------------------------------------------------------------------------------- 1 | %button { 2 | align-items: center; 3 | background: $button-bg; 4 | border: 1px solid $border-color; 5 | border-radius: $br; 6 | color: $text-color; 7 | display: flex; 8 | font: inherit; 9 | justify-content: center; 10 | min-height: g(1.5); 11 | min-width: g(1.5); 12 | padding: calc(g(.25) - 1px); 13 | text-align: center; 14 | text-decoration: none; 15 | user-select: none; 16 | vertical-align: middle; 17 | 18 | &:not(:disabled) { 19 | cursor: pointer; 20 | 21 | &:active, 22 | &:focus, 23 | &:hover { 24 | background: $button-hover-bg; 25 | border-color: $border-color; 26 | color: $text-color; 27 | } 28 | 29 | &:active { 30 | box-shadow: $inset-shadow; 31 | } 32 | 33 | &:focus-visible { 34 | z-index: 1; 35 | } 36 | } 37 | 38 | &[disabled] { 39 | opacity: .3; 40 | } 41 | } 42 | 43 | %button-active { 44 | background: $link-color; 45 | color: $text-color-inverted; 46 | z-index: 1; 47 | 48 | &:not(:disabled) { 49 | &:active, 50 | &:focus, 51 | &:hover { 52 | background: $link-hover-color; 53 | color: $text-color-inverted; 54 | } 55 | } 56 | } 57 | 58 | %button-borderless { 59 | @extend %button; 60 | background: none; 61 | border: 1px solid transparent; 62 | box-shadow: none; 63 | margin: 0; // Safari fix 64 | 65 | &:not(:disabled) { 66 | &:active { 67 | box-shadow: $inset-shadow; 68 | } 69 | } 70 | } 71 | 72 | %button-small { 73 | @extend %button; 74 | font-size: $font-size-small; 75 | min-height: g(1.3333); 76 | min-width: g(1.3333); 77 | padding: 0 g(.75); 78 | } 79 | 80 | %button-translucent { 81 | @extend %button; 82 | backdrop-filter: $blur; 83 | background: oklch(from $panel-bg l c h / 38.2%); 84 | border: 1px solid $border-color; 85 | box-shadow: none; 86 | overflow: hidden; 87 | 88 | &:not(:disabled) { 89 | &:active, 90 | &:focus, 91 | &:hover { 92 | background: oklch(from $panel-bg l c h / 61.8%); 93 | } 94 | 95 | &:active { 96 | box-shadow: $inset-shadow; 97 | } 98 | } 99 | 100 | &[disabled] { 101 | opacity: 1; 102 | 103 | > * { 104 | opacity: .3; 105 | } 106 | } 107 | 108 | > * { 109 | filter: drop-shadow(0 0 .5px $panel-bg); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/styles/extends/iiif-html.scss: -------------------------------------------------------------------------------- 1 | // See https://iiif.io/api/presentation/3.0/#45-html-markup-in-property-values 2 | // Styles for these elements might not be set by the parent website, so we set 3 | // some basic styling to ensure consistency. 4 | 5 | %iiif-html { 6 | a { 7 | // Nothing to do here 8 | } 9 | 10 | b { 11 | font-weight: bold; 12 | } 13 | 14 | i { 15 | font-style: italic; 16 | } 17 | 18 | img { 19 | display: block; 20 | margin: g(.5) 0; 21 | } 22 | 23 | small { 24 | font-size: smaller; 25 | } 26 | 27 | sub { 28 | font-size: smaller; 29 | vertical-align: sub; 30 | } 31 | 32 | sup { 33 | font-size: smaller; 34 | vertical-align: super; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/extends/panel.scss: -------------------------------------------------------------------------------- 1 | %panel { 2 | background: $panel-bg; 3 | flex: 1; 4 | inset: 0; 5 | overflow-y: auto; 6 | padding: g(.5); 7 | position: absolute; 8 | 9 | @container (#{$medium}) { 10 | min-width: g(15); 11 | position: relative; 12 | 13 | .tify-media ~ & { 14 | border-left: 1px solid $border-color; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/functions/g.scss: -------------------------------------------------------------------------------- 1 | @function g($factor: 1) { 2 | @if $factor == 1 { 3 | @return $grid-base; 4 | } 5 | 6 | @return calc(#{$grid-base} * #{$factor}); 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'functions/*'; 2 | @import 'util/settings'; 3 | @import 'mixins/*'; 4 | @import 'extends/*'; 5 | @import 'util/base'; 6 | @import 'sections/*'; 7 | -------------------------------------------------------------------------------- /src/styles/mixins/input.scss: -------------------------------------------------------------------------------- 1 | @mixin input { 2 | background: none; 3 | border: 1px solid $border-color; 4 | border-radius: $br; 5 | color: inherit; 6 | font: inherit; 7 | line-height: inherit; 8 | padding: calc(g(.25) - 1px) g(.375); 9 | 10 | &:focus, 11 | &:hover { 12 | border-color: $link-color; 13 | outline: 0; 14 | } 15 | 16 | &:focus { 17 | box-shadow: 0 0 0 1px $base-color-pale; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/mixins/range.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on Styling Cross-Browser Compatible Range Inputs with Sass v1.4.1 3 | * Github: https://github.com/darlanrod/input-range-sass 4 | * Author: Darlan Rod https://github.com/darlanrod 5 | * MIT License 6 | */ 7 | 8 | // NOTE: Styles with different vendor prefixes must not be grouped 9 | 10 | $track-color: $border-color linear-gradient( 11 | to right, 12 | // NOTE: Using color-mix() instead of oklch() for Firefox 128 ESR 13 | color-mix(in oklch, $link-color, transparent), 14 | $link-color var(--value), 15 | transparent var(--value) 16 | ); 17 | $track-focus-color: null; 18 | $track-height: g(.25); 19 | $track-radius: $br; 20 | 21 | @mixin range-thumb { 22 | opacity: 0; 23 | width: 0; 24 | } 25 | 26 | @mixin range-track { 27 | height: $track-height; 28 | width: 100%; 29 | } 30 | 31 | @mixin range { 32 | appearance: none; 33 | background: none; 34 | cursor: pointer; 35 | height: $track-height; 36 | margin: $track-height 0; 37 | width: 100%; 38 | 39 | &::-webkit-slider-runnable-track { 40 | @include range-track; 41 | background: $track-color; 42 | border-radius: $track-radius; 43 | margin: 0; 44 | } 45 | 46 | &::-webkit-slider-thumb { 47 | @include range-thumb; 48 | } 49 | 50 | &::-moz-range-track { 51 | @include range-track; 52 | background: $track-color; 53 | border: 0; 54 | border-radius: $track-radius; 55 | } 56 | 57 | &:focus-visible::-moz-range-track, 58 | &:hover::-moz-range-track { 59 | height: calc(#{$track-height} * 2); 60 | margin: calc(#{$track-height} * -.5) 0; 61 | } 62 | 63 | &::-moz-range-thumb { 64 | @include range-thumb; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/sections/button-list.scss: -------------------------------------------------------------------------------- 1 | .tify-button-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1px; 5 | list-style: none; 6 | margin: 0; 7 | padding: 0; 8 | 9 | > li { 10 | margin: 0; 11 | padding: 0; 12 | 13 | > a, 14 | > button { 15 | @extend %button-borderless; 16 | display: flex; 17 | justify-content: start; 18 | padding: g(.125) g(.375); 19 | text-align: left; 20 | width: 100%; 21 | } 22 | 23 | > button[aria-pressed=true] { 24 | @extend %button-active; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/sections/collection.scss: -------------------------------------------------------------------------------- 1 | .tify-collection { 2 | @extend %panel; 3 | overflow-y: scroll; 4 | padding: g(.5); 5 | } 6 | 7 | .tify-collection-controls { 8 | display: flex; 9 | 10 | > :first-child { 11 | border-radius: $br 0 0 $br; 12 | } 13 | 14 | > :last-child { 15 | border-radius: 0 $br $br 0; 16 | } 17 | } 18 | 19 | .tify-collection-filter { 20 | flex: 1; 21 | max-width: 36em; 22 | 23 | &:focus { 24 | z-index: 1; 25 | } 26 | } 27 | 28 | .tify-collection-list { 29 | list-style: none; 30 | margin: 0; 31 | padding: 0; 32 | 33 | & & { 34 | margin-left: g(); 35 | margin-top: g(.25); 36 | } 37 | 38 | li { 39 | margin-bottom: g(.25); 40 | } 41 | } 42 | 43 | .tify-collection-link { 44 | @extend %button; 45 | align-items: start; 46 | gap: .2em; 47 | justify-content: left; 48 | padding-left: g(.375); 49 | padding-right: g(.375); 50 | text-align: left; 51 | text-decoration: none; 52 | width: 100%; 53 | 54 | &.-has-children { 55 | font-weight: bold; 56 | } 57 | 58 | .tify-collection-item.-current & { 59 | @extend %button-active; 60 | color: $text-color-inverted !important; 61 | } 62 | 63 | > .tify-icon { 64 | margin-left: -.25em; 65 | } 66 | } 67 | 68 | .tify-collection-no-results { 69 | color: $text-color-muted; 70 | } 71 | 72 | .tify-collection-reset { 73 | @extend %button-small; 74 | margin-left: -1px; 75 | } 76 | -------------------------------------------------------------------------------- /src/styles/sections/dropdown.scss: -------------------------------------------------------------------------------- 1 | .tify-dropdown { 2 | position: relative; 3 | } 4 | 5 | .tify-dropdown-content { 6 | $bw: clamp(1px, $br, 4px); // border-width 7 | background: $dropdown-bg; 8 | border: 1px solid $border-color; 9 | border-radius: $br; 10 | filter: drop-shadow($drop-shadow); 11 | min-width: g(4); 12 | overflow: visible; 13 | padding: $bw; 14 | position: absolute; 15 | text-shadow: none; 16 | z-index: 9; 17 | 18 | &.-bottom { 19 | left: 50%; 20 | margin: g(.25) 0; 21 | top: 100%; 22 | transform: translateX(-50%); 23 | } 24 | 25 | &.-right { 26 | left: 100%; 27 | margin: 0 g(.25); 28 | top: 50%; 29 | transform: translateY(-50%); 30 | } 31 | 32 | &.-top { 33 | bottom: 100%; 34 | left: 50%; 35 | margin: g(.25) 0; 36 | transform: translateX(-50%); 37 | } 38 | 39 | // Wedge 40 | &::before { 41 | background: $dropdown-bg; 42 | border: 1px solid $border-color; 43 | clip-path: polygon(0 0, 100% 0, 0 100%); 44 | content: ''; 45 | display: block; 46 | height: g(.5); 47 | pointer-events: none; 48 | position: absolute; 49 | width: g(.5); 50 | } 51 | 52 | &.-bottom::before { 53 | left: 50%; 54 | top: g(-.25); 55 | transform: translateX(-50%) rotate(45deg); 56 | } 57 | 58 | &.-right::before { 59 | left: g(-.25); 60 | top: 50%; 61 | transform: translateY(-50%) rotate(-45deg); 62 | } 63 | 64 | &.-top::before { 65 | bottom: g(-.25); 66 | left: 50%; 67 | transform: translateX(-50%) rotate(225deg); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/sections/error.scss: -------------------------------------------------------------------------------- 1 | .tify-error { 2 | background: #d22c; 3 | border-radius: 0 $br 0 0; 4 | bottom: 0; 5 | color: #fff; 6 | display: flex; 7 | font-weight: bold; 8 | max-height: 50%; 9 | max-width: 90%; 10 | position: absolute; 11 | 12 | p { 13 | margin: 0; 14 | } 15 | } 16 | 17 | .tify-error-close { 18 | align-self: start; 19 | background: 0; 20 | border: 0; 21 | color: #fff; 22 | cursor: pointer; 23 | display: flex; 24 | padding: g(.25); 25 | 26 | &:focus, 27 | &:hover { 28 | background: #0002; 29 | } 30 | } 31 | 32 | .tify-error-messages { 33 | overflow: auto; 34 | padding: g(.25) g(.5) g(.25) 0; 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/sections/export.scss: -------------------------------------------------------------------------------- 1 | .tify-export { 2 | @extend %panel; 3 | } 4 | 5 | .tify-export-container { 6 | margin: g(.5) 0 g(); 7 | position: relative; 8 | } 9 | 10 | .tify-export-link { 11 | @extend %button; 12 | align-items: normal; 13 | color: $text-color !important; 14 | overflow: hidden; 15 | padding: 0; 16 | } 17 | 18 | .tify-export-link-format { 19 | font-size: $font-size-small; 20 | line-height: 1; 21 | margin: auto 0 0 auto; 22 | white-space: nowrap; 23 | } 24 | 25 | .tify-export-link-hint { 26 | color: $text-color; 27 | font-size: $font-size-small; 28 | } 29 | 30 | .tify-export-link-media { 31 | background: $highlight-bg; 32 | padding: g(.5); 33 | text-align: center; 34 | 35 | img { 36 | display: block; 37 | height: g(4.5); 38 | object-fit: contain; 39 | width: g(4); 40 | } 41 | } 42 | 43 | .tify-export-link-text { 44 | align-items: start; 45 | display: flex; 46 | flex: 1; 47 | flex-direction: column; 48 | gap: g(.25); 49 | padding: g(.5); 50 | text-align: left; 51 | } 52 | 53 | .tify-export-list { 54 | list-style: none; 55 | margin: 0; 56 | padding: 0; 57 | 58 | li { 59 | margin-bottom: g(.5); 60 | } 61 | } 62 | 63 | .tify-export-section { 64 | margin: 0 0 g(); 65 | } 66 | 67 | .tify-export-toc { 68 | border-radius: $br; 69 | box-shadow: 0 0 0 1px $border-color inset; 70 | margin: g(.5) 0 0; 71 | padding: g(.25); 72 | position: relative; 73 | 74 | h4 { 75 | margin: g(.25) g(.75); 76 | } 77 | 78 | ul { 79 | margin: 0 0 0 g(.5); 80 | padding: 0; 81 | } 82 | } 83 | 84 | .tify-export-toggle { 85 | @extend %button-small; 86 | 87 | &.-close { 88 | @extend %button; 89 | border-radius: 0 $br; 90 | position: absolute; 91 | right: 0; 92 | z-index: 1; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/styles/sections/help.scss: -------------------------------------------------------------------------------- 1 | .tify-help { 2 | @extend %panel; 3 | overflow-y: auto; 4 | } 5 | 6 | .tify-help-footer { 7 | box-shadow: 0 1px $border-color inset; 8 | font-size: $font-size-small; 9 | margin-top: g(); 10 | padding-top: g(.5); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/sections/icon.scss: -------------------------------------------------------------------------------- 1 | .tify-icon { 2 | fill: currentcolor; 3 | flex: 0 0 24px; 4 | height: 24px; 5 | vertical-align: middle; 6 | 7 | &.-spin { 8 | animation: rotate 1s linear infinite; 9 | } 10 | 11 | @keyframes rotate { 12 | 100% { 13 | transform: rotate(360deg); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/sections/info.scss: -------------------------------------------------------------------------------- 1 | $info-content-max-height: g(6.75); 2 | 3 | .tify-info { 4 | @extend %panel; 5 | overflow-y: auto; 6 | padding-bottom: g(); 7 | } 8 | 9 | .tify-info-button { 10 | @extend %button-small; 11 | border-radius: 0; 12 | 13 | &[aria-pressed=true] { 14 | @extend %button-active; 15 | } 16 | 17 | &:first-child { 18 | border-radius: $br 0 0 $br; 19 | } 20 | 21 | &:last-child { 22 | border-radius: 0 $br $br 0; 23 | } 24 | } 25 | 26 | .tify-info-content { 27 | position: relative; 28 | 29 | &.-collapsed { 30 | max-height: $info-content-max-height; 31 | } 32 | } 33 | 34 | .tify-info-header { 35 | display: inline-flex; 36 | position: relative; 37 | } 38 | 39 | .tify-info-image-labels { 40 | font-size: $font-size-small; 41 | list-style: none; 42 | margin: g(.5) 0; 43 | } 44 | 45 | .tify-info-logo { 46 | display: block; 47 | height: auto; 48 | max-height: g(6); 49 | max-width: g(12); 50 | width: auto; 51 | } 52 | 53 | .tify-info-metadata { 54 | .tify-info-section.-pages & { 55 | margin-top: g(.5); 56 | } 57 | 58 | > div { 59 | margin-bottom: g(.5); 60 | } 61 | } 62 | 63 | .tify-info-section { 64 | @extend %iiif-html; 65 | margin: g() 0; 66 | overflow-wrap: break-word; 67 | 68 | &:first-of-type { 69 | margin-top: 0; 70 | } 71 | 72 | &.-logo { 73 | // Ensure visibility in dark mode since most logos require a white background 74 | background: $panel-bg; 75 | color-scheme: light; 76 | margin: g(-.5); 77 | padding: g(.5); 78 | 79 | > :last-child { 80 | margin: 0; 81 | } 82 | } 83 | 84 | &.-pages { 85 | .tify-page-name { 86 | font-weight: bold; 87 | } 88 | } 89 | 90 | &.-title { 91 | > p { 92 | font-weight: bold; 93 | } 94 | } 95 | } 96 | 97 | .tify-info-toggle { 98 | @extend %button-small; 99 | margin: g(.5) 0; 100 | 101 | > .tify-icon { 102 | margin-left: -.5em; 103 | } 104 | } 105 | 106 | .tify-info-value { 107 | @extend %iiif-html; 108 | 109 | > div:last-child > :last-child { 110 | margin-bottom: 0; 111 | } 112 | 113 | .tify-info-content.-collapsed & { 114 | max-height: calc(#{$info-content-max-height} - g(2)); // 2 = button height 115 | overflow: hidden; 116 | 117 | &::after { 118 | background: $panel-bg; 119 | bottom: g(1 + .75); // = button height + margin 120 | content: ''; 121 | font-size: $font-size-small; 122 | height: g(2); 123 | mask-image: linear-gradient(transparent, #000); 124 | position: absolute; 125 | width: 100%; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/styles/sections/list.scss: -------------------------------------------------------------------------------- 1 | .tify-list { 2 | list-style: disc outside; 3 | margin: 0 0 g(.5); 4 | padding: 0 0 0 g(); 5 | 6 | &:has(> li:only-child), 7 | &.-unstyled { 8 | list-style: none; 9 | padding: 0; 10 | } 11 | 12 | li { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/sections/loading.scss: -------------------------------------------------------------------------------- 1 | .tify-loading { 2 | animation: fadein .5s forwards .5s, spin 1.2s infinite ease-in-out; 3 | background: $base-color; 4 | border-radius: $br; 5 | bottom: g(2.5); 6 | height: g(1.75); 7 | left: g(2.5); 8 | opacity: 0; 9 | pointer-events: none; 10 | position: absolute; 11 | width: g(1.75); 12 | 13 | @keyframes fadein { 14 | 0% { 15 | opacity: 0; 16 | } 17 | 18 | 100% { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | @keyframes spin { 24 | 0% { 25 | transform: perspective(120px) rotateX(0deg) rotateY(0deg); 26 | } 27 | 28 | 50% { 29 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 30 | } 31 | 32 | 100% { 33 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/sections/main.scss: -------------------------------------------------------------------------------- 1 | .tify-main { 2 | display: flex; 3 | flex: 1; 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/sections/page-name.scss: -------------------------------------------------------------------------------- 1 | .tify-page-name { 2 | display: block; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | width: 100%; 7 | 8 | &.-wrap { 9 | white-space: normal; 10 | } 11 | 12 | > span { 13 | color: $text-color-muted; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/sections/page-select.scss: -------------------------------------------------------------------------------- 1 | .tify-page-select { 2 | > .tify-dropdown-button { 3 | @extend %button; 4 | height: 100%; 5 | line-height: 1.5; 6 | max-width: g(5); 7 | min-width: g(3); 8 | overflow: hidden; 9 | padding-left: g(.375); 10 | padding-right: g(.375); 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | 14 | @container (#{$small}) { 15 | max-width: g(8); 16 | min-width: g(4); 17 | } 18 | 19 | @container (#{$medium}) { 20 | max-width: g(10); 21 | } 22 | 23 | @container (#{$large}) { 24 | max-width: g(12); 25 | } 26 | } 27 | 28 | &:not(:only-child) > .tify-dropdown-button { 29 | border-radius: $br 0 0 $br; 30 | 31 | &:not(:focus, :hover) { 32 | border-right: 0; 33 | padding-right: calc(g(.375) + 1px); 34 | } 35 | } 36 | 37 | > .tify-dropdown-content { 38 | max-width: g(10); 39 | min-width: 100%; 40 | 41 | @container (#{$large}) { 42 | max-width: g(15); 43 | } 44 | } 45 | } 46 | 47 | .tify-page-select-filter { 48 | margin: 0 0 clamp(1px, $br, 4px); 49 | } 50 | 51 | .tify-page-select-input { 52 | width: 100%; 53 | } 54 | 55 | .tify-page-select-list { 56 | max-height: g(13.5); 57 | min-width: 100%; 58 | overflow-y: scroll; 59 | position: relative; // For scroll calculation 60 | 61 | > li > a { 62 | &.-current { 63 | @extend %button-active; 64 | } 65 | 66 | &.-highlighted { 67 | border-color: light-dark(black, white); 68 | } 69 | } 70 | 71 | .tify-page-name { 72 | width: max-content; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/sections/player.scss: -------------------------------------------------------------------------------- 1 | .tify-player { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | overflow: hidden; 6 | width: 100%; 7 | 8 | &.-audio { 9 | align-items: center; 10 | justify-content: center; 11 | padding: g(.5); 12 | } 13 | 14 | &.-bottom { 15 | height: calc(g(2) + 1px); 16 | margin-top: auto; 17 | } 18 | } 19 | 20 | .tify-player-av { 21 | background: black; 22 | cursor: pointer; 23 | flex: 1; 24 | height: calc(100% - g(2) - 1px); 25 | transition: height $td; 26 | z-index: 0; 27 | 28 | &::cue { 29 | // TODO: Style subtitles 30 | } 31 | } 32 | 33 | .tify-player-controls { 34 | background: $header-bg; 35 | border-top: 1px solid $border-color; 36 | display: flex; 37 | flex-wrap: wrap; 38 | gap: g(.25); 39 | justify-content: center; 40 | padding: g(.25); 41 | transition: margin $td; 42 | width: 100%; 43 | z-index: 99; 44 | 45 | .tify-player.-audio & { 46 | border: 1px solid $border-color; 47 | border-radius: $br; 48 | max-width: g(32); 49 | } 50 | 51 | :fullscreen .tify-player.-playing:not(.-mousing) & { 52 | margin-bottom: calc(g(-2) - 1px); 53 | } 54 | 55 | > div { 56 | align-items: center; 57 | display: flex; 58 | flex: 1 auto; 59 | gap: g(.25); 60 | margin: 0; 61 | min-width: 0; 62 | 63 | &:last-child { 64 | flex: 0 1 g(5); 65 | justify-self: end; 66 | } 67 | } 68 | } 69 | 70 | .tify-player-duration { 71 | color: $text-color-muted; 72 | font-size: $font-size-small; 73 | } 74 | 75 | .tify-player-overlay { 76 | align-items: center; 77 | background: none; 78 | border: 0; 79 | color: #fff; 80 | display: flex; 81 | inset: 0 0 g(2); 82 | justify-content: center; 83 | pointer-events: none; 84 | position: absolute; 85 | transition: opacity $td; 86 | z-index: 1; 87 | 88 | &.-hidden { 89 | opacity: 0; 90 | } 91 | 92 | .tify-icon { 93 | flex: 0 0 g(4); 94 | height: auto; 95 | } 96 | } 97 | 98 | .tify-player-mute { 99 | @extend %button-borderless; 100 | } 101 | 102 | .tify-player-play-pause { 103 | @extend %button-borderless; 104 | 105 | .tify-player.-audio & { 106 | padding: calc(g(.5) - 1px); 107 | } 108 | } 109 | 110 | .tify-player-seekbar { 111 | flex: 1; 112 | margin: 0; 113 | min-width: g(3); 114 | } 115 | 116 | .tify-player-select { 117 | // font-size: $font-size-small; 118 | 119 | .tify-dropdown-button { 120 | @extend %button-borderless; 121 | flex-direction: column; 122 | min-height: g(1.5); 123 | min-width: g(1.5); 124 | padding: 0; 125 | } 126 | 127 | &.-rate button { 128 | justify-content: center; 129 | } 130 | } 131 | 132 | .tify-player-select-badge { 133 | font-size: $font-size-small; 134 | line-height: g(.5); 135 | } 136 | 137 | h3.tify-player-select-title { 138 | box-shadow: none; 139 | margin: 0; 140 | padding: 0 g(.25); 141 | white-space: nowrap; 142 | 143 | &::after { 144 | content: none; 145 | } 146 | } 147 | 148 | .tify-player-time { 149 | margin: 0 g(.25); 150 | white-space: nowrap; 151 | } 152 | 153 | .tify-player-volume { 154 | flex: 1; 155 | padding-right: g(.5); 156 | } 157 | -------------------------------------------------------------------------------- /src/styles/sections/sr-only.scss: -------------------------------------------------------------------------------- 1 | .tify-sr-only { 2 | border: 0; 3 | clip: rect(0, 0, 0, 0); 4 | height: 1px; 5 | margin: -1px; 6 | overflow: hidden; 7 | padding: 0; 8 | position: absolute; 9 | width: 1px; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/sections/text.scss: -------------------------------------------------------------------------------- 1 | .tify-text { 2 | @extend %panel; 3 | overflow-y: auto; 4 | } 5 | 6 | .tify-text-item { 7 | margin: 0 g(-.75); 8 | padding: 0 g(.5); 9 | 10 | &.-current { 11 | box-shadow: g(-.25) 0 $base-color-pale; 12 | color: $link-color; 13 | } 14 | 15 | img { 16 | // Ensure visibility in dark mode since images with transparency may require a white background 17 | background: $panel-bg; 18 | color-scheme: light; 19 | height: auto; 20 | margin: g(.5) 0; 21 | max-width: 100%; 22 | } 23 | } 24 | 25 | .tify-text-toggle { 26 | @extend %iiif-html; 27 | border-radius: $br; 28 | cursor: pointer; 29 | display: block; 30 | overflow-wrap: break-word; 31 | padding: g(.25); 32 | text-decoration: none; 33 | 34 | &:focus, 35 | &:hover { 36 | background: $base-color-paler; 37 | color: $link-color; 38 | 39 | // Element label and page number 40 | > span { 41 | background: $base-color-paler; 42 | } 43 | } 44 | 45 | p { 46 | margin: 0; 47 | } 48 | } 49 | 50 | .tify-text-list { 51 | list-style: none; 52 | padding: 0 !important; 53 | } 54 | 55 | .tify-text-none { 56 | color: $text-color-muted; 57 | font-style: italic; 58 | } 59 | 60 | .tify-text-page { 61 | margin: 0 g(.25) g() g(.5); 62 | 63 | .tify-page-name { 64 | width: auto; 65 | } 66 | 67 | .tify-page-name-number:not(:only-child) { 68 | margin-left: 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/styles/sections/thumbnails.scss: -------------------------------------------------------------------------------- 1 | .tify-thumbnails { 2 | @extend %panel; 3 | min-height: 100%; 4 | overflow-y: scroll; // NOTE: This is required for thumbnails to be calculated correctly 5 | position: relative; 6 | user-select: none; 7 | } 8 | 9 | .tify-thumbnails-button { 10 | @extend %button; 11 | color: inherit; 12 | display: block; 13 | overflow: hidden; 14 | padding: 0; 15 | text-decoration: none; 16 | width: calc($thumbnail-width + 2px); 17 | 18 | .tify-thumbnails-item.-current :is(&, &:active) { 19 | box-shadow: 0 0 0 g(.5 * .3333) $base-color-pale; 20 | } 21 | } 22 | 23 | .tify-thumbnails-image { 24 | align-items: center; 25 | background: $highlight-bg; 26 | display: flex; 27 | height: $thumbnail-height; 28 | justify-content: center; 29 | object-fit: contain; 30 | width: 100%; 31 | } 32 | 33 | .tify-thumbnails-item { 34 | display: block; 35 | float: left; 36 | line-height: g(); 37 | margin: g(.25); 38 | 39 | .tify-page-name { 40 | font-size: $font-size-small; 41 | padding: 0 g(.25); 42 | } 43 | 44 | &.-current .tify-page-name { 45 | background: $link-color; 46 | color: $text-color-inverted; 47 | } 48 | } 49 | 50 | .tify-thumbnails-list { 51 | margin: g(-.25); 52 | padding: 0; 53 | } 54 | -------------------------------------------------------------------------------- /src/styles/sections/toc.scss: -------------------------------------------------------------------------------- 1 | %toc-row-item { 2 | background: $panel-bg; 3 | position: relative; 4 | z-index: 1; 5 | } 6 | 7 | .tify-toc { 8 | @extend %panel; 9 | overflow-y: auto; 10 | position: relative; 11 | z-index: 0; 12 | } 13 | 14 | .tify-toc-header { 15 | display: flex; 16 | margin: 0 g(.25) g(.5); 17 | } 18 | 19 | .tify-toc-label { 20 | @extend %toc-row-item; 21 | padding-right: .2em; 22 | } 23 | 24 | .tify-toc-link { 25 | border-radius: $br; 26 | color: $link-color; 27 | cursor: pointer; 28 | display: block; 29 | overflow: hidden; 30 | padding: g(.25); 31 | position: relative; 32 | text-decoration: none; 33 | 34 | &:focus, 35 | &:hover { 36 | background: $base-color-paler; 37 | color: $link-hover-color; 38 | 39 | // Element label and page number 40 | > span { 41 | background: $base-color-paler; 42 | } 43 | } 44 | 45 | &.-dots { 46 | // Dotted line below 47 | &::after { 48 | border-top: 1px dotted; 49 | bottom: g(.5); 50 | content: ''; 51 | left: g(.25); 52 | min-width: 4em; 53 | position: absolute; 54 | right: g(.25); 55 | } 56 | } 57 | } 58 | 59 | .tify-toc-list { 60 | margin: 0 0 g(.25) g(.25); 61 | padding: 0; 62 | position: relative; 63 | z-index: 0; 64 | 65 | & & { 66 | // Make space for vertical connector to the left 67 | margin: 0 0 0 g(1.25); 68 | } 69 | 70 | a { 71 | border: 0; 72 | box-shadow: none; 73 | } 74 | } 75 | 76 | .tify-toc-page { 77 | @extend %toc-row-item; 78 | float: right; 79 | padding-left: .2em; 80 | z-index: 1; 81 | } 82 | 83 | .tify-toc-structure { 84 | display: block; 85 | margin: 0; // For smooth embedding 86 | position: relative; 87 | 88 | &.-current { 89 | // Bold vertical marker 90 | box-shadow: g(-.5) 0 $panel-bg, g(-.75) 0 $base-color-pale; 91 | } 92 | 93 | &.-expanded { 94 | // Vertical connector to the left 95 | &::after { 96 | border-left: 1px solid $base-color-pale; 97 | content: ''; 98 | height: 100%; 99 | left: calc(g(.75) - 1px); 100 | position: absolute; 101 | top: g(.25); 102 | z-index: -2; 103 | } 104 | 105 | &:last-child::after { 106 | height: calc(100% - g()); 107 | } 108 | } 109 | 110 | & & { 111 | // Horizontal connector 112 | &::before { 113 | border-top: 1px solid $base-color-pale; 114 | content: ''; 115 | display: block; 116 | height: 100%; 117 | left: calc(g(-.5) - 1px); 118 | position: absolute; 119 | top: g(.75); 120 | width: calc(g(.5) + 1px); 121 | } 122 | 123 | // Prevent vertical line from protruding at the bottom 124 | &:last-child::before { 125 | background: $panel-bg; 126 | } 127 | } 128 | } 129 | 130 | .tify-toc-toggle-all { 131 | @extend %button-small; 132 | margin: g(.25); 133 | } 134 | 135 | .tify-toc-toggle { 136 | @extend %button-small; 137 | float: left; 138 | margin: g(.25) 0 0 g(.25); 139 | padding: 0; 140 | position: relative; 141 | 142 | // Gap between vertical line and button 143 | &::after { 144 | border-top: calc(g(.25) + 1px) solid $panel-bg; 145 | content: ''; 146 | pointer-events: none; 147 | position: absolute; 148 | top: 100%; 149 | width: g(); 150 | z-index: -1; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/styles/util/base.scss: -------------------------------------------------------------------------------- 1 | .tify { 2 | background: $main-bg; 3 | box-sizing: border-box; 4 | color: $text-color; 5 | color-scheme: light dark; 6 | container-type: size; 7 | display: flex; 8 | flex-direction: column; 9 | font-size: $font-size; 10 | height: 100%; 11 | line-height: $line-height; 12 | min-height: 240px; 13 | min-width: 240px; 14 | overflow: hidden; 15 | position: relative; 16 | -webkit-tap-highlight-color: transparent; 17 | text-wrap: balance; 18 | 19 | &.-light { 20 | color-scheme: light; 21 | } 22 | 23 | &.-dark { 24 | color-scheme: dark; 25 | } 26 | 27 | *, 28 | *::before, 29 | *::after { 30 | box-sizing: inherit; 31 | } 32 | 33 | // For communicating container size to JS 34 | &::after { 35 | pointer-events: none; 36 | position: absolute; 37 | visibility: hidden; 38 | 39 | @container (#{$small}) { 40 | content: 'small'; 41 | } 42 | 43 | @container (#{$medium}) { 44 | content: 'small medium'; 45 | } 46 | 47 | @container (#{$large}) { 48 | content: 'small medium large'; 49 | } 50 | } 51 | 52 | a:not([class]) { 53 | color: $link-color; 54 | word-wrap: break-word; 55 | 56 | &:focus, 57 | &:hover { 58 | color: $link-hover-color; 59 | } 60 | } 61 | 62 | b { 63 | font-weight: bold; 64 | } 65 | 66 | h3 { 67 | align-items: baseline; 68 | color: $text-color-muted; 69 | display: flex; 70 | font-size: $font-size-small; 71 | font-weight: bold; 72 | gap: .5em; 73 | letter-spacing: .1em; 74 | margin: 0 0 g(.5); 75 | padding: 0; 76 | text-transform: uppercase; 77 | 78 | &::after { 79 | background: $border-color; 80 | content: ''; 81 | flex: 1; 82 | height: 1px; 83 | } 84 | } 85 | 86 | h4 { 87 | color: $text-color-muted; 88 | font-size: 1em; 89 | font-weight: normal; 90 | margin: 0; 91 | padding: 0; 92 | 93 | &:nth-of-type(n + 2) { 94 | margin-top: g(.5); 95 | } 96 | } 97 | 98 | label { 99 | cursor: pointer; 100 | font-size: inherit; 101 | font-weight: normal; 102 | } 103 | 104 | p { 105 | margin: 0 0 g(.5); 106 | padding: 0; 107 | } 108 | 109 | [type=range] { 110 | @include range; 111 | } 112 | 113 | [type=text] { 114 | @include input; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: [ 4 | // NOTE: Order of extends matters 5 | '@stylistic/stylelint-config', 6 | 'stylelint-config-standard-scss', 7 | 'stylelint-config-recommended-vue/scss', 8 | ], 9 | plugins: [ 10 | 'stylelint-order', 11 | ], 12 | rules: { 13 | 'declaration-empty-line-before': [ 14 | 'never', 15 | { ignore: ['after-declaration'] }, 16 | ], 17 | 'order/order': [ 18 | 'custom-properties', 19 | 'declarations', 20 | ], 21 | 'order/properties-alphabetical-order': true, 22 | 'selector-attribute-quotes': 'never', 23 | 'selector-class-pattern': [ 24 | '^(-?[a-z][a-z0-9-]*[a-z0-9])$', 25 | ], 26 | '@stylistic/block-closing-brace-newline-after': [ 27 | 'always', 28 | { ignoreAtRules: ['if', 'else'] }, 29 | ], 30 | '@stylistic/indentation': 'tab', 31 | '@stylistic/number-leading-zero': 'never', 32 | '@stylistic/string-quotes': 'single', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /tests/e2e/api.spec.js: -------------------------------------------------------------------------------- 1 | // TODO: Extend API tests 2 | 3 | describe('API', () => { 4 | it('controls TIFY programmatically', () => { 5 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json`); 6 | 7 | // NOTE: Cypress’ afterEach hook (used for HTML validation) breaks tests 8 | // within nested promises for unknown reasons, so we use a small hack to 9 | // exclude it from API tests below. 10 | cy.bypassAfterEach = true; 11 | 12 | cy.window().its('tify').then((tify) => { 13 | tify.ready.then(() => { 14 | tify.viewer.viewport.zoomTo(4); 15 | cy.get('[title="Zoom in"]').should('be.disabled'); 16 | 17 | tify.setLanguage('de'); 18 | cy.contains('Seiten'); 19 | 20 | tify.setPage(2); 21 | cy.contains('.tify-page-name', '2 · -'); 22 | 23 | tify.setView('export'); 24 | cy.contains('[aria-expanded="true"]', 'Export'); 25 | 26 | tify.setLanguage('de'); 27 | cy.contains('.tify-header-button', 'Seiten'); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/e2e/collection.spec.js: -------------------------------------------------------------------------------- 1 | describe('Collection', () => { 2 | it('shows the collection view', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json`); 4 | 5 | // Only Info and Collection buttons should be visible 6 | cy.contains('Info'); 7 | cy.contains('Collection'); 8 | cy.should('not.contain', 'Media'); 9 | cy.should('not.contain', 'Text'); 10 | cy.should('not.contain', 'Contents'); 11 | cy.should('not.contain', 'Export'); 12 | 13 | cy.contains('Volume 1, 1859').click(); 14 | cy.contains('15. September 1859').click(); 15 | cy.contains('The chemist and druggist, 15. September 1859'); 16 | }); 17 | 18 | it('filters the collection list', () => { 19 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json`); 20 | 21 | cy.get('[aria-label="Filter collection"]').type('2008'); 22 | cy.get('.tify-collection-item').should('have.length', 2); 23 | cy.get('[aria-label="Filter collection"]').type('nope'); 24 | cy.contains('No results'); 25 | }); 26 | 27 | it('loads a child manifest', () => { 28 | const encodedParams = encodeURIComponent(JSON.stringify({ 29 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760_1_0004.json`, 30 | })); 31 | 32 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json&tify=${encodedParams}`); 33 | 34 | cy.contains('Info').click(); 35 | cy.contains('.tify-info-button', 'Collection').click(); 36 | cy.contains('h1', 'The chemist and druggist, 15. September 1859'); 37 | 38 | cy.contains('.tify-header-button', 'Collection').click(); 39 | cy.contains('Volume 1').click(); 40 | cy.contains('15. October 1859').click(); 41 | cy.contains('h1', 'The chemist and druggist, 15. October 1859'); 42 | }); 43 | 44 | it('highlights the current child manifest', () => { 45 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json`); 46 | 47 | cy.contains('Volume 1').click(); 48 | cy.contains('15. September 1859').click(); 49 | cy.contains('.tify-collection-item.-current', '15. September 1859'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/e2e/content-state.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Content State', () => { 2 | it('supports setting the manifest URL via URL query "iiif-content"', () => { 3 | cy.visit(`/?iiif-content=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json`); 4 | 5 | cy.contains('De Supputatione Multitudinis'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/e2e/export.spec.js: -------------------------------------------------------------------------------- 1 | describe('Export', () => { 2 | it('displays export links', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json`); 4 | 5 | cy.contains('Export').click(); 6 | cy.contains('Media Files').should('be.visible'); 7 | cy.contains('a[download]', '1 · -').should('be.visible'); // NOTE: Page set by startCanvas 8 | 9 | cy.get('[title="Next page"]').first().click(); 10 | cy.contains('a[download]', '2 · -').should('be.visible'); 11 | 12 | cy.contains('PDFs for each element').click(); 13 | cy.contains('Titelseite').should('be.visible'); 14 | 15 | cy.contains('a', 'IIIF manifest'); 16 | 17 | cy.get('a[href$="/download/pdf/PPN857449303/LOG_0001.pdf"]'); 18 | 19 | cy.contains('Other Formats').next().find('a').should('have.length', 4); 20 | }); 21 | 22 | it('hides "Other Formats" if not available', () => { 23 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/aku-pal-375.json`); 24 | 25 | cy.contains('Export').click(); 26 | 27 | cy.should('not.contain', 'Other Formats'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0001-mvm-image.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0001: Simple single-page manifest without image service', () => { 2 | before(() => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0001-mvm-image/manifest.json`); 4 | }); 5 | 6 | it('shows the title and the canvas', () => { 7 | cy.contains('h1', 'Single Image Example'); 8 | cy.get('.openseadragon-canvas').should('be.visible'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0002-mvm-audio.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0002: Simple audio file', () => { 2 | before(() => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0002-mvm-audio/manifest.json`); 4 | }); 5 | 6 | it('shows the audio player', () => { 7 | cy.contains('h1', 'Simplest Audio Example 1'); 8 | cy.get('audio'); 9 | 10 | // TODO: Extend 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0003-mvm-video.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0003: Simple video file', () => { 2 | before(() => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0003-mvm-video/manifest.json`); 4 | }); 5 | 6 | it('shows the audio player', () => { 7 | cy.contains('h1', 'Video Example 3'); 8 | cy.get('video').should('be.visible'); 9 | 10 | // TODO: Extend 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0010-book-2-viewing-direction.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0010: Viewing direction', () => { 2 | it('supports right-to-left manifests', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0010-book-2-viewing-direction/manifest-rtl.json`); 4 | cy.contains('header', 'front cover'); 5 | 6 | cy.get('header [title="Next page"]:visible').click(); 7 | cy.contains('header', 'pages 1–2'); // That’s an n-dash 8 | 9 | cy.get('.tify-media-button.-left[title="Next page"]').click(); 10 | cy.contains('header', 'pages 3–4'); 11 | 12 | cy.get('header [title="Last page"]:visible').click(); 13 | cy.contains('header', 'back cover'); 14 | 15 | // Buttons should be vertically mirrored 16 | cy.get('.tify-header-button-group.-pagination') 17 | .should('have.css', 'transform', 'matrix(-1, 0, 0, -1, 0, 0)'); 18 | }); 19 | 20 | it('supports top-to-bottom manifests', () => { 21 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0010-book-2-viewing-direction/manifest-ttb.json`); 22 | cy.contains('header', 'image 1'); 23 | 24 | cy.get('header [title="Next page"]:visible').click(); 25 | cy.contains('header', 'image 2'); 26 | 27 | cy.get('.tify-media-button.-bottom[title="Next page"]').click(); 28 | cy.contains('header', 'image 3'); 29 | 30 | cy.get('.tify-media-button.-top[title="Previous page"]').click(); 31 | cy.contains('header', 'image 2'); 32 | 33 | // Icons should be rotated 90deg 34 | cy.get('.tify-header-button-group.-pagination svg, [title="Toggle double-page"] svg') 35 | .should('have.css', 'transform', 'matrix(0, 1, -1, 0, 0, 0)'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0019-html-in-annotations.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0019: HTML in annotations', () => { 2 | before(() => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0019-html-in-annotations/manifest.json` 5 | + '&tify={"view":"text"}', 6 | ); 7 | }); 8 | 9 | it('displays the annotation', () => { 10 | cy.contains('.tify-text-toggle', 'Gänseliesel Brunnen'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0021-tagging.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0019: HTML in annotations', () => { 2 | it('displays the annotation overlay', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0021-tagging/manifest.json` 5 | + '&tify={"view":"text"}', 6 | ); 7 | cy.contains('Gänseliesel-Brunnen').click(); 8 | cy.get('.tify-media-overlay').should('have.length', 1).should('have.class', '-current'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0033-choice.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0033: Multiple choice of images in a single view (canvas)', () => { 2 | it('can switch layers with the layers button', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0033-choice/manifest.json`); 4 | 5 | cy.contains('John Dee performing an experiment before Queen Elizabeth I.'); 6 | 7 | cy.get('[title="Toggle image layer select"]') 8 | .click(); 9 | 10 | cy.contains('[aria-pressed=true]', 'Natural Light'); 11 | cy.contains('[aria-pressed=false]', 'X-Ray') 12 | .click(); 13 | 14 | cy.get('.tify-media-dropdown.-layers.-active > button') 15 | .click(); 16 | 17 | cy.contains('[aria-pressed=false]', 'Natural Light'); 18 | cy.contains('[aria-pressed=true]', 'X-Ray'); 19 | }); 20 | 21 | it('remembers the selected layer', () => { 22 | const encodedParams = encodeURIComponent(JSON.stringify({ 23 | layers: [1], 24 | })); 25 | 26 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0033-choice/manifest.json&tify=${encodedParams}`); 27 | 28 | cy.get('.tify-media-dropdown.-layers.-active > button') 29 | .click(); 30 | 31 | cy.contains('[aria-pressed=false]', 'Natural Light'); 32 | cy.contains('[aria-pressed=true]', 'X-Ray'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0035-foldouts.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0025: Foldout as separate page in double-page view', () => { 2 | it('supports non-paged pages in paged manifests', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0035-foldouts/manifest.json`); 4 | 5 | cy.contains('header', 'Front cover'); 6 | cy.get('[title="Toggle double-page"][aria-pressed=true]'); 7 | 8 | cy.contains('Pages').click(); 9 | cy.get('.tify-thumbnails-item.-current:nth-child(1)'); 10 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 11 | 12 | cy.get('[title="Next page"]').eq(0).click(); 13 | cy.get('.tify-thumbnails-item.-current:nth-child(2)'); 14 | cy.get('.tify-thumbnails-item.-current:nth-child(3)'); 15 | cy.get('.tify-thumbnails-item.-current').should('have.length', 2); 16 | 17 | cy.get('[title="Next page"]').eq(0).click(); 18 | cy.get('.tify-thumbnails-item.-current:nth-child(4)'); 19 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 20 | 21 | cy.get('[title="Next page"]').eq(0).click(); 22 | cy.get('.tify-thumbnails-item.-current:nth-child(5)'); 23 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 24 | 25 | cy.get('[title="Next page"]').eq(0).click(); 26 | cy.get('.tify-thumbnails-item.-current:nth-child(6)'); 27 | cy.get('.tify-thumbnails-item.-current:nth-child(7)'); 28 | cy.get('.tify-thumbnails-item.-current').should('have.length', 2); 29 | 30 | cy.contains('.tify-thumbnails-item', 'Foldout, unfolded').click(); 31 | cy.get('.tify-thumbnails-item.-current:nth-child(4)'); 32 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 33 | 34 | cy.contains('.tify-thumbnails-item', 'Inside front cover').click(); 35 | cy.get('.tify-thumbnails-item.-current').should('have.length', 2); 36 | 37 | cy.get('[title="Toggle double-page"]').click(); 38 | cy.get('.tify-thumbnails-item.-current:nth-child(3)'); 39 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 40 | 41 | cy.get('.tify-thumbnails-item:nth-child(4)').click(); 42 | cy.get('[title="Toggle double-page"]').click(); 43 | cy.get('.tify-thumbnails-item.-current').should('have.length', 1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0266-full-canvas-annotation.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0266: Full-canvas annotation', () => { 2 | it('does not display an annotation overlay', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0266-full-canvas-annotation/manifest.json` 5 | + '&tify={"view":"text"}', 6 | ); 7 | cy.get('.tify-media-overlay').should('not.exist'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0283-missing-image.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0283: Missing image', () => { 2 | it('handles a missing thumbnail', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0283-missing-image/manifest.json` 5 | + '&tify={"view":"thumbnails"}', 6 | ); 7 | 8 | cy.get('.tify-thumbnails-item').should('have.length', 4); 9 | cy.contains('.tify-thumbnails-item:eq(1)', 'Image missing'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/e2e/iiif-cookbook/0377-image-in-annotation.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Cookbook 0377: Image in annotations', () => { 2 | it('displays images in annotations', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/iiif-cookbook/0377-image-in-annotation/manifest.json` 5 | + '&tify={"view":"text"}', 6 | ); 7 | 8 | cy.get('.tify-text img[src$="918ecd18c2592080851777620de9bcb5-fountain/full/300,/0/default.jpg"]'); 9 | cy.contains('.tify-text', 'Night picture of the Gänseliesel fountain'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/e2e/iiif3.spec.js: -------------------------------------------------------------------------------- 1 | describe('Support for native IIIF 3 manifests', () => { 2 | it('displays correct metadata', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifests/aku-pal-375.json` 5 | + '&tify={"view":"info"}', 6 | ); 7 | 8 | cy.contains('h1', 'Dipinto Assiut, Gebel Assiut al-gharbi, N13.1 TN2'); 9 | cy.contains('a', 'AKU-PAL database record'); 10 | }); 11 | 12 | it('displays the TOC', () => { 13 | cy.visit( 14 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifests/amherst-63cc6105-570d-407b-af8c-07fda3f8c620.json` 15 | + '&tify={"view":"toc"}', 16 | ); 17 | 18 | cy.get('.tify-toc-structure.-current:not(.-expanded)'); 19 | cy.contains('.tify-toc-link', 'Transcription of Emily Dickinson\'s "I\'ll clutch - and clutch" - Image 3'); 20 | cy.get('.tify-toc-link').should('have.length', 1); 21 | }); 22 | 23 | it('displays attribution HTML', () => { 24 | cy.visit( 25 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifests/mskgent-8210.json` 26 | + '&tify={"view":"info"}', 27 | ); 28 | 29 | cy.get('.tify-info-section.-attribution').find('img'); // logo in attribution section 30 | }); 31 | 32 | it('displays a dash for metadata with an empty value', () => { 33 | cy.visit( 34 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifests/utrecht-1874-325480.json` 35 | + '&tify={"view":"info"}', 36 | ); 37 | 38 | cy.contains('h4', 'Published').next().should('have.text', '‒' /* $n/a = figure dash */); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/e2e/info.spec.js: -------------------------------------------------------------------------------- 1 | describe('Info', () => { 2 | it('displays related metadata', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b18035723.json`); 4 | cy.contains('Info').click(); 5 | cy.should('not.contain', 'Provided by'); 6 | cy.contains('Wunder der Vererbung / von Fritz Bolle.'); 7 | 8 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/ubl-0000000001.json`); 9 | cy.contains('Info').click(); 10 | cy.should('not.contain', 'Provided by'); 11 | cy.contains('/object/viewid/0000000001'); 12 | cy.contains('/0000000001/manifest'); 13 | 14 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/digitale-sammlungen-bsb00026283.json`); 15 | cy.contains('Info').click(); 16 | cy.get('.tify-info-section.-related li').should('have.length', 2); 17 | cy.get('a[href$="/details:bsb00026283"]').contains('Details'); 18 | cy.get('a[href$="/title/BV023398264"]').contains('OPAC'); 19 | }); 20 | 21 | it('collapses long metadata values', () => { 22 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json`); 23 | cy.contains('button', 'Expand'); 24 | }); 25 | 26 | it('shows metadata of the current structure', () => { 27 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json`); 28 | 29 | cy.contains('Info').click(); 30 | cy.contains('.tify-info', 'Current Section').should('be.visible'); 31 | cy.contains('.tify-info', 'Titelseite'); 32 | 33 | cy.get('[title="Toggle double-page"]').click(); 34 | cy.get('[title="Last page"]').first().click(); 35 | cy.contains('Current Section').should('not.exist'); 36 | }); 37 | 38 | it('shows metadata of a nested structure', () => { 39 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-DE_611_BF_5619_1801_1806.json`); 40 | 41 | cy.get('[title="Toggle double-page"]').click(); 42 | 43 | cy.contains('Info').click(); 44 | Cypress._.times(4, () => cy.get('[title="Next page"]').first().click()); 45 | cy.contains('.tify-info', 'Current Section').should('be.visible'); 46 | cy.contains( 47 | '.tify-info-section.-metadata.-structure', 48 | '[Brief des Barons von Asch an Heyne vom 29.01./10.02.1801]', 49 | ); 50 | }); 51 | 52 | it('displays all provider information', () => { 53 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b24738918.json`); 54 | cy.contains('Info').click(); 55 | 56 | cy.fixture('../../iiif-api/data/manifests/wellcome-b24738918.json').then((manifest) => { 57 | const nbsp = '\u00A0'; 58 | const separator = `${nbsp}· `; 59 | const providerStringWithoutUrl = manifest.provider[0].label.en.slice(0, -1).join(separator); 60 | cy.get('.tify-info-section.-provider p').should('contain.text', providerStringWithoutUrl); 61 | }); 62 | }); 63 | 64 | it('only displays a related homepage once for converted IIIF 2 manifests', () => { 65 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN140716181.json`); 66 | cy.contains('Info').click(); 67 | cy.get('.tify-info-section.-related a[href$="/DB=1/PPN?PPN=140716181"]').contains('OPAC'); 68 | cy.get('.tify-info-section.-provider').should('not.exist'); 69 | }); 70 | 71 | // TODO: Add test for manifests/biblhertz-garofalo-ligorio-comparison.json 72 | // Check image labels in info view 73 | }); 74 | -------------------------------------------------------------------------------- /tests/e2e/main.spec.js: -------------------------------------------------------------------------------- 1 | describe('Main', () => { 2 | it('starts the app', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json`); 4 | cy.get('.tify'); 5 | }); 6 | 7 | it('checks the manifest (valid JSON, but not IIIF)', () => { 8 | // Prevent the test from failing due to an uncaught exception (which is expected) 9 | cy.on('uncaught:exception', () => false); 10 | 11 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/invalid.json`); 12 | cy.contains('Please provide a valid IIIF Presentation API manifest'); 13 | }); 14 | 15 | it('checks the manifest (invalid JSON)', () => { 16 | // Prevent the test from failing due to an uncaught exception (which is expected) 17 | cy.on('uncaught:exception', () => false); 18 | 19 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/not-json.json`); 20 | cy.contains('Error loading IIIF manifest'); 21 | }); 22 | 23 | it('loads a translation', () => { 24 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json&language=de`); 25 | 26 | cy.get('.tify-header'); 27 | cy.contains('Seiten'); 28 | cy.contains('Inhalt'); 29 | }); 30 | 31 | it('gracefully handles a missing translation', () => { 32 | // Prevent the test from failing due to an uncaught exception (which is expected) 33 | cy.on('uncaught:exception', () => false); 34 | 35 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json&language=nope`); 36 | 37 | cy.get('.tify-header'); 38 | cy.contains('Pages'); 39 | cy.contains('Contents'); 40 | cy.contains('Error loading translation “nope”'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/e2e/media.spec.js: -------------------------------------------------------------------------------- 1 | describe('Media', () => { 2 | it('uses image filters', () => { 3 | const encodedParams = encodeURIComponent(JSON.stringify({ 4 | filters: { 5 | saturate: 0, 6 | }, 7 | })); 8 | 9 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json&tify=${encodedParams}`); 10 | cy.get('[title="Toggle image filters"]').click(); 11 | cy.get('.tify-media-dropdown').contains('Saturation 0'); 12 | }); 13 | 14 | it('resets pan, zoom, rotation and filters at once', () => { 15 | const encodedParams = encodeURIComponent(JSON.stringify({ 16 | filters: { 17 | brightness: 1.1, 18 | contrast: 0.9, 19 | saturate: 1.1, 20 | }, 21 | pan: { 22 | x: 0.5, 23 | y: 0.5, 24 | }, 25 | rotation: 90, 26 | zoom: 2, 27 | })); 28 | 29 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json&tify=${encodedParams}`); 30 | 31 | cy.get('[title="Rotate"].-active'); 32 | cy.get('.tify-dropdown.-active [title="Toggle image filters"]'); 33 | 34 | cy.get('.tify').type('{shift}0'); 35 | cy.url().should( 36 | 'include', 37 | `/?manifest=${encodeURIComponent(`${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json`)}`, 38 | ); 39 | }); 40 | 41 | it('controls the image via keyboard', () => { 42 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json`); 43 | 44 | // Zoom 45 | cy.contains('Reset').should('be.disabled'); 46 | cy.get('.tify').type('+'); 47 | cy.get('[title="Reset"]').should('not.be.disabled'); 48 | cy.get('.tify').type('+=WWW'); 49 | cy.get('[title="Zoom in"]').should('be.disabled'); 50 | cy.get('.tify').type('-'); 51 | cy.get('[title="Zoom in"]').should('not.be.disabled'); 52 | cy.get('.tify').type('-_SSS'); 53 | cy.get('[title="Zoom out"]').should('be.disabled'); 54 | 55 | // Pan 56 | cy.get('.tify').type('0', { delay: 500 }); 57 | cy.get('[title="Reset"]').should('be.disabled'); 58 | cy.get('.tify').type('wd'); 59 | cy.get('[title="Reset"]').should('not.be.disabled'); 60 | cy.get('.tify').type('sa'); 61 | cy.get('[title="Reset"]').should('be.disabled'); 62 | 63 | // Rotate 64 | cy.get('.tify').type('r'); 65 | cy.get('[title="Rotate"].-active').should('be.visible'); 66 | cy.get('.tify').type('r'); 67 | cy.get('.tify').type('r'); 68 | cy.get('.tify').type('r'); 69 | cy.get('[title="Rotate"]:not(.-active)').should('be.visible'); 70 | 71 | // Filter 72 | cy.get('.tify').type('i'); 73 | cy.contains('Brightness').should('be.visible') 74 | .type('i'); 75 | cy.contains('Brightness').should('not.be.visible'); 76 | cy.get('.tify').type('i'); 77 | cy.contains('Brightness').should('be.visible') 78 | .type('{esc}'); 79 | cy.contains('Brightness').should('not.be.visible'); 80 | }); 81 | 82 | it('shows only usable pagination buttons', () => { 83 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json`); 84 | 85 | cy.get('.tify-media-button.-left').should('not.exist'); 86 | cy.get('.tify-media-button.-right'); 87 | 88 | cy.get('[title="Last page"]:visible').click(); 89 | 90 | cy.get('.tify-media-button.-left'); 91 | cy.get('.tify-media-button.-right').should('not.exist'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/e2e/multi-instance.spec.js: -------------------------------------------------------------------------------- 1 | describe('Multi-instance', () => { 2 | it('starts multiple independent apps with different manifests', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN857449303.json` 4 | + `&manifest2=${Cypress.env('iiifApiUrl')}/manifests/gdz-HANS_DE_7_w042081.json`); 5 | 6 | cy.contains('De Supputatione Multitudinis'); 7 | cy.contains('Algebra : Vorlesungsmanuskript'); 8 | 9 | cy.get('[title="View"]').eq(0).click(); 10 | cy.get('[title="Next page"]:visible').eq(0).click(); 11 | cy.get('.tify-page-select > button').eq(0).contains('2 · -'); 12 | cy.get('.tify-page-select > button').eq(1).contains('1 · -'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/e2e/support/e2e.js: -------------------------------------------------------------------------------- 1 | import 'cypress-html-validate/commands'; 2 | 3 | afterEach(() => { 4 | if (cy.bypassAfterEach) { 5 | return; 6 | } 7 | 8 | cy.htmlvalidate( 9 | { 10 | rules: { 11 | 'heading-level': [ 12 | 'error', 13 | { 14 | allowMultipleH1: true, 15 | }, 16 | ], 17 | 'long-title': 'off', 18 | 'prefer-native-element': 'off', 19 | 'require-sri': 'off', 20 | 'valid-id': [ 21 | 'error', 22 | { 23 | relaxed: true, 24 | }, 25 | ], 26 | }, 27 | }, 28 | { 29 | exclude: [ 30 | // Annotation overlays may contain duplicate IDs 31 | '.openseadragon-canvas', 32 | // Attribution may contain invalid HTML if the manifest provides such 33 | '.tify-info-section.-attribution', 34 | ], 35 | }, 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/text.spec.js: -------------------------------------------------------------------------------- 1 | describe('Text (annotations)', () => { 2 | it('displays annotations and overlays at the correct positions', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b18035723.json&tify={"pages":[14,15,16]}`); 4 | 5 | cy.contains('Text').click(); 6 | cy.contains('Alles höhere Leben - ob Tier oder').should('be.visible'); 7 | 8 | cy.get('.tify-media-overlay').should('have.length', 102); 9 | 10 | // TODO: Test first page in double-page mode 11 | 12 | // Check the first annotation overlay of each page 13 | cy.get('[style*="left: 8.79344px; top: 206.345px"]') 14 | .children('.tify-media-overlay[style*="width: 53.1397px; height: 2.34997px"]'); 15 | cy.get('[style*="left: 193.089px; top: 206.345px"]') 16 | .children('.tify-media-overlay[style*="width: 53.1397px; height: 2.34997px"]'); 17 | cy.get('[style*="left: 377.384px; top: 206.345px"]') 18 | .children('.tify-media-overlay[style*="width: 53.1397px; height: 2.34997px"]'); 19 | }); 20 | 21 | it('loads and displays an annotation list', () => { 22 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/harvard-art-museum-299843.json`); 23 | 24 | cy.get('[title="Toggle annotations"]').should('not.exist'); 25 | 26 | cy.contains('Text').click(); 27 | 28 | cy.get('[title="Toggle annotations"]').click(); 29 | cy.get('.tify-media-overlay').should('have.length', 5).should('not.be.visible'); 30 | cy.get('[title="Toggle annotations"]').click(); 31 | cy.get('.tify-media-overlay').should('have.length', 5).should('be.visible'); 32 | 33 | cy.contains('.tify-text-toggle', 'Painting'); 34 | cy.contains('.tify-text-toggle', 'Person'); 35 | }); 36 | 37 | it('displays XML annotations', () => { 38 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/gdz-PPN235181684_0029.json&tify={"view":"text"}`); 39 | 40 | cy.contains('Unter Mitwirkung der Herren').should('be.visible'); 41 | }); 42 | 43 | it('highlights the corresponding text when an overlay is clicked and vice versa', () => { 44 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b18035723.json&tify={"view":"text"}`); 45 | 46 | cy.get('.tify-media-overlay:eq(22)').click(); 47 | cy.get('.tify-media-overlay:eq(22)').should('have.class', '-current'); 48 | cy.contains('.tify-text-item.-current', 'näher kommen'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/e2e/views.spec.js: -------------------------------------------------------------------------------- 1 | describe('Views', () => { 2 | it('changes the view via buttons', () => { 3 | const encodedParams = encodeURIComponent(JSON.stringify({ 4 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760_1_0004.json`, 5 | })); 6 | 7 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json&tify=${encodedParams}`); 8 | 9 | cy.contains('Text').click(); 10 | cy.contains('[aria-expanded="true"]', 'Text'); 11 | 12 | cy.contains('Pages').click(); 13 | cy.contains('[aria-expanded="true"]', 'Pages'); 14 | 15 | cy.contains('Contents').click(); 16 | cy.contains('[aria-expanded="true"]', 'Contents'); 17 | 18 | cy.contains('Info').click(); 19 | cy.contains('[aria-expanded="true"]', 'Info'); 20 | 21 | cy.contains('Export').click(); 22 | cy.contains('[aria-expanded="true"]', 'Export'); 23 | 24 | cy.contains('.tify-header-button', 'Collection').click(); 25 | cy.contains('[aria-expanded="true"]', 'Collection'); 26 | 27 | cy.contains('Help').click(); 28 | cy.contains('[aria-expanded="true"]', 'Help'); 29 | }); 30 | 31 | it('changes the view via keyboard', () => { 32 | const encodedParams = encodeURIComponent(JSON.stringify({ 33 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760_1_0004.json`, 34 | })); 35 | 36 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifests/wellcome-b19974760.json&tify=${encodedParams}`); 37 | 38 | cy.contains('The chemist and druggist'); 39 | 40 | cy.get('.tify').type('1'); 41 | cy.contains('[aria-expanded="true"]', 'Text'); 42 | 43 | cy.get('.tify').type('2'); 44 | cy.contains('[aria-expanded="true"]', 'Pages'); 45 | 46 | cy.get('.tify').type('3'); 47 | cy.contains('[aria-expanded="true"]', 'Contents'); 48 | 49 | cy.get('.tify').type('4'); 50 | cy.contains('[aria-expanded="true"]', 'Info'); 51 | 52 | cy.get('.tify').type('5'); 53 | cy.contains('[aria-expanded="true"]', 'Export'); 54 | 55 | cy.get('.tify').type('6'); 56 | cy.contains('[aria-expanded="true"]', 'Collection'); 57 | 58 | cy.get('.tify').type('7'); 59 | cy.contains('[aria-expanded="true"]', 'Help'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/iiif-api/data/annotation-lists/gdz-PPN235181684_0029-00000001.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/2/context.json", 3 | "@id": "https://manifests.sub.uni-goettingen.de/iiif/presentation/PPN235181684_0029/list/gdz:PPN235181684_0029:00000001", 4 | "@type": "sc:AnnotationList", 5 | "resources": [ 6 | { 7 | "@context": "http://iiif.io/api/presentation/2/context.json", 8 | "@type": "oa:Annotation", 9 | "motivation": "sc:painting", 10 | "resource": { 11 | "@id": "https://gdz.sub.uni-goettingen.de/fulltext/PPN235181684_0029/00000001.xml", 12 | "@type": "dctypes:Text", 13 | "format": "text/html" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/iiif-api/data/fixtures/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/tests/iiif-api/data/fixtures/sample.mp4 -------------------------------------------------------------------------------- /tests/iiif-api/data/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/25b7fbbb82c476e3336f300daa29fdd03a66ec9d/tests/iiif-api/data/images/default.jpg -------------------------------------------------------------------------------- /tests/iiif-api/data/infos/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/2/context.json", 3 | "@id": "{{ id.path }}", 4 | "width": 1732, 5 | "height": 2236, 6 | "protocol": "http://iiif.io/api/image", 7 | "sizes": [ 8 | { 9 | "height": 140, 10 | "width": 109 11 | }, 12 | { 13 | "height": 280, 14 | "width": 217 15 | }, 16 | { 17 | "height": 559, 18 | "width": 433 19 | }, 20 | { 21 | "height": 1118, 22 | "width": 866 23 | }, 24 | { 25 | "height": 2236, 26 | "width": 1732 27 | } 28 | ], 29 | "tiles": [ 30 | { 31 | "width": 1024, 32 | "height": 1024, 33 | "scaleFactors": [ 34 | 1, 35 | 2, 36 | 4, 37 | 8, 38 | 16 39 | ] 40 | } 41 | ], 42 | "profile": "http://iiif.io/api/image/2/level1" 43 | } 44 | -------------------------------------------------------------------------------- /tests/iiif-api/data/infos/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "extraFormats": [ 4 | "jpg", 5 | "png" 6 | ], 7 | "extraQualities": [ 8 | "default", 9 | "color", 10 | "gray" 11 | ], 12 | "height": 3024, 13 | "id": "{{ id.path }}", 14 | "profile": "level1", 15 | "protocol": "http://iiif.io/api/image", 16 | "tiles": [ 17 | { 18 | "height": 512, 19 | "scaleFactors": [ 20 | 1, 21 | 2, 22 | 4 23 | ], 24 | "width": 512 25 | } 26 | ], 27 | "type": "ImageService3", 28 | "width": 4032 29 | } 30 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/aku-pal-375.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "homepage": [ 4 | { 5 | "format": "text/html", 6 | "id": "https://aku-pal.uni-mainz.de/objects/375", 7 | "label": { 8 | "en": [ 9 | "AKU-PAL database record" 10 | ] 11 | }, 12 | "type": "Text" 13 | } 14 | ], 15 | "id": "https://aku-pal.uni-mainz.de/api/manifests/cae2c87f-52d0-408e-9e87-508e73baa954", 16 | "items": [ 17 | { 18 | "height": 3743, 19 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141", 20 | "items": [ 21 | { 22 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141/page/p1/1", 23 | "items": [ 24 | { 25 | "body": { 26 | "format": "image/jpeg", 27 | "height": 3743, 28 | "id": "https://aku-pal.uni-mainz.de:8183/iiif/3/data%2Fdig%2Fdig_10.tif/full/max/0/default.jpg", 29 | "type": "Image", 30 | "width": 5613 31 | }, 32 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141/annotation/p1", 33 | "motivation": "painting", 34 | "target": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141", 35 | "type": "Annotation" 36 | } 37 | ], 38 | "type": "AnnotationPage" 39 | } 40 | ], 41 | "label": { 42 | "none": [ 43 | "Dipinto" 44 | ] 45 | }, 46 | "type": "Canvas", 47 | "width": 5613 48 | } 49 | ], 50 | "label": { 51 | "en": [ 52 | "Dipinto Assiut, Gebel Assiut al-gharbi, N13.1 TN2" 53 | ] 54 | }, 55 | "logo": { 56 | "id": "https://www.aegyptologie.uni-mainz.de/files/2014/08/Logo_neu.jpg", 57 | "type": "Image" 58 | }, 59 | "metadata": [ 60 | { 61 | "label": { 62 | "de": [ 63 | "Objekt" 64 | ], 65 | "en": [ 66 | "Object" 67 | ] 68 | }, 69 | "value": { 70 | "de": [ 71 | "Dipinto" 72 | ], 73 | "en": [ 74 | "Dipinto" 75 | ] 76 | } 77 | }, 78 | { 79 | "label": { 80 | "de": [ 81 | "Standort" 82 | ], 83 | "en": [ 84 | "Location" 85 | ] 86 | }, 87 | "value": { 88 | "de": [ 89 | "Assiut, Gebel Assiut al-gharbi, N13.1 TN2" 90 | ], 91 | "en": [ 92 | "Asyut, Gebel Asyut al-gharbi, N13.1 TN2" 93 | ] 94 | } 95 | }, 96 | { 97 | "label": { 98 | "de": [ 99 | "Datierung" 100 | ], 101 | "en": [ 102 | "Date" 103 | ] 104 | }, 105 | "value": { 106 | "de": [ 107 | "Neues Reich, 18. Dynastie" 108 | ], 109 | "en": [ 110 | "New Kingdom, 18th Dynasty" 111 | ] 112 | } 113 | } 114 | ], 115 | "requiredStatement": { 116 | "label": { 117 | "en": [ 118 | "Attribution" 119 | ] 120 | }, 121 | "value": { 122 | "en": [ 123 | "The Asyut Project, photo: Fritz Barthel" 124 | ] 125 | } 126 | }, 127 | "rights": "http://rightsstatements.org/vocab/InC/1.0/", 128 | "type": "Manifest" 129 | } 130 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0019-html-in-annotations.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": { 54 | "type": "TextualBody", 55 | "language": "de", 56 | "format": "text/html", 57 | "value": "

Göttinger Marktplatz mit Gänseliesel Brunnen Wikipedia logo

" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0021-tagging.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/page/p1/1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/annotation/p0001-image", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/page/p2/1", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/annotation/p0002-tag", 51 | "type": "Annotation", 52 | "motivation": "tagging", 53 | "body": { 54 | "type": "TextualBody", 55 | "value": "Gänseliesel-Brunnen", 56 | "language": "de", 57 | "format": "text/plain" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1#xywh=265,661,1260,1239" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0266-full-canvas-annotation.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": { 54 | "type": "TextualBody", 55 | "language": "de", 56 | "format": "text/plain", 57 | "value": "Göttinger Marktplatz mit Gänseliesel Brunnen" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0377-image-in-annotation.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": [ 54 | { 55 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-fountain/full/300,/0/default.jpg", 56 | "type": "Image", 57 | "format": "image/jpeg" 58 | }, 59 | { 60 | "type": "TextualBody", 61 | "language": "en", 62 | "value": "Night picture of the Gänseliesel fountain in Göttingen taken during the 2019 IIIF Conference" 63 | } 64 | ], 65 | "target": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1#xywh=138,550,1477,1710" 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/invalid.json: -------------------------------------------------------------------------------- 1 | "This is not a valid IIIF manifest." 2 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/not-json.json: -------------------------------------------------------------------------------- 1 | This is not valid JSON. 2 | -------------------------------------------------------------------------------- /tests/unit/App.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | 3 | import pkg from '../../package.json'; 4 | 5 | import App from '../../src/App.vue'; 6 | 7 | import api from '../../src/plugins/api'; 8 | import defaultOptions from '../../src/config'; 9 | import store from '../../src/plugins/store'; 10 | import i18n from '../../src/plugins/i18n'; 11 | 12 | describe('setLanguage', () => { 13 | // Mock getComputedStyle, which does not exist in Node 14 | window.getComputedStyle = () => ({ content: '' }); 15 | 16 | const { vm } = mount(App, { 17 | global: { 18 | plugins: [ 19 | [api, { instance: this }], 20 | i18n, 21 | store, 22 | ], 23 | }, 24 | props: { 25 | readyPromise: {}, 26 | }, 27 | }); 28 | 29 | vm.$store.options = defaultOptions; 30 | vm.$store.options.translationsDirUrl = 'translations'; 31 | 32 | // Replace fetchJson to return a mock object 33 | vm.$store.fetchJson = (url) => new Promise((resolve, reject) => { 34 | if (url === `translations/de.json?${pkg.version}`) { 35 | resolve({ $language: 'Deutsch' }); 36 | } else { 37 | reject(new Error()); 38 | } 39 | }); 40 | 41 | it('loads the translation and changes the language', async () => { 42 | const result = await vm.setLanguage('de'); 43 | expect(result).toEqual('de'); 44 | }); 45 | 46 | it('throws an error when the translation cannot be loaded', async () => { 47 | try { 48 | await vm.setLanguage('-_-'); 49 | } catch { 50 | expect(vm.$store.errors.values().next()).toContain('Error loading translation for “-_-”'); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/unit/components/PageSelect.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | 3 | import PageSelect from '../../../src/components/PageSelect.vue'; 4 | 5 | import i18n from '../../../src/plugins/i18n'; 6 | import id from '../../../src/plugins/id'; 7 | import store from '../../../src/plugins/store'; 8 | 9 | import manifest from '../../iiif-api/data/manifests/utrecht-1874-325480.json'; 10 | 11 | describe('PageSelect', () => { 12 | const { vm } = mount(PageSelect, { 13 | global: { 14 | plugins: [ 15 | i18n, 16 | id, 17 | [store, { 18 | manifest, 19 | options: { 20 | language: 'en', 21 | pageLabelFormat: 'P : L', 22 | pages: [1], 23 | }, 24 | rootElement: { addEventListener() {} }, 25 | }], 26 | ], 27 | }, 28 | }); 29 | 30 | it('filters and updates canvases', () => { 31 | vm.filter = '10'; 32 | vm.updateFilteredCanvases(); 33 | 34 | expect(vm.highlightIndex).toEqual(0); 35 | expect(vm.filteredCanvases.length).toEqual(12); // Should contain pages 5, 15, 25, 35 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/components/ViewToc.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | 3 | import ViewToc from '../../../src/components/ViewToc.vue'; 4 | 5 | import i18n from '../../../src/plugins/i18n'; 6 | import store from '../../../src/plugins/store'; 7 | 8 | import manifestForLabels from '../../iiif-api/data/manifests/digitale-sammlungen-bsb00026283.json'; 9 | import manifestForPages from '../../iiif-api/data/manifests/gdz-DE_611_BF_5619_1801_1806.json'; 10 | 11 | describe('ViewToc', () => { 12 | const { vm } = mount(ViewToc, { 13 | global: { 14 | plugins: [ 15 | i18n, 16 | [store, { 17 | options: { language: 'en' }, 18 | manifest: manifestForLabels, 19 | }], 20 | ], 21 | }, 22 | }); 23 | 24 | it('selects a label in the current language', () => { 25 | const label = vm.$store.localize(vm.$store.structures[0].label); 26 | expect(label).toEqual('Miniatur: Jesu Gebet in Gethsemane'); 27 | }); 28 | 29 | it('orders pages by logical page number', () => { 30 | vm.$store.manifest = manifestForPages; 31 | 32 | const pages = vm.$store.structures[0].canvases.map((structure) => structure.firstPage); 33 | expect(pages.toString()).toEqual(pages.sort((a, b) => a - b).toString()); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/modules/filenamify.spec.js: -------------------------------------------------------------------------------- 1 | import { filenamifyUrl } from '../../../src/demo/modules/filenamify'; 2 | 3 | describe('filenamifyUrl', () => { 4 | it('turns a URL into a safe filename', () => { 5 | const url = 'https://example.org/path?query=something#hash'; 6 | expect(filenamifyUrl(url)).toEqual('example.org-path-query=something#hash'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/unit/modules/filter.spec.js: -------------------------------------------------------------------------------- 1 | import { filterHtml } from '../../../src/modules/filter'; 2 | 3 | describe('filterHtml', () => { 4 | it('filters HTML', () => { 5 | const html = ` 6 |

7 | 8 | label 9 | 10 |

11 |

12 | keep 13 |
14 | keep this 15 | Text 16 |

17 | Text 18 | `; 19 | 20 | const filteredHtml = ` 21 | ${''} 22 | 23 | label 24 | 25 | ${''} 26 |

27 | keep 28 |
29 | keep this 30 | Text 31 |

32 | Text 33 | `; 34 | 35 | expect(filterHtml(html)).toEqual(filteredHtml); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/modules/validation.spec.js: -------------------------------------------------------------------------------- 1 | import { isValidPagesArray, isValidUrl } from '../../../src/modules/validation'; 2 | 3 | describe('isValidPagesArray', () => { 4 | it('validates page numbers', () => { 5 | const pageCount = 5; 6 | 7 | expect(isValidPagesArray([1], pageCount)).toEqual(true); 8 | expect(isValidPagesArray([0, 1], pageCount)).toEqual(true); 9 | expect(isValidPagesArray([5, -1], pageCount)).toEqual(true); 10 | expect(isValidPagesArray([1, 3, 5], pageCount)).toEqual(true); 11 | 12 | expect(isValidPagesArray(['nope'], pageCount)).toEqual(false); 13 | expect(isValidPagesArray([1, 1], pageCount)).toEqual(false); 14 | expect(isValidPagesArray([-2], pageCount)).toEqual(false); 15 | expect(isValidPagesArray([999], pageCount)).toEqual(false); 16 | expect(isValidPagesArray([5, 3, 1], pageCount)).toEqual(false); 17 | }); 18 | }); 19 | 20 | describe('isValidUrl', () => { 21 | it('validates a URL', () => { 22 | const validUrl = 'http://example.org'; 23 | expect(isValidUrl(validUrl)).toEqual(true); 24 | 25 | const validUrlWithQuery = 'https://example.org/?query=something'; 26 | expect(isValidUrl(validUrlWithQuery)).toEqual(true); 27 | 28 | const notAUrl = 'example.org'; 29 | expect(isValidUrl(notAUrl)).toEqual(false); 30 | 31 | const notAHttpUrl = 'ftp://example.js'; 32 | expect(isValidUrl(notAHttpUrl)).toEqual(false); 33 | expect(isValidUrl(notAHttpUrl, ['ftp:'])).toEqual(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/plugins/id.spec.js: -------------------------------------------------------------------------------- 1 | import id from '../../../src/plugins/id'; 2 | 3 | const app = { config: { globalProperties: {} } }; 4 | id.install(app); 5 | 6 | describe('getId', () => { 7 | it('generates unique IDs', () => { 8 | expect(app.config.globalProperties.$getId('label')).toMatch(/[a-z0-9-]{36}-label/); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/plugins/store.spec.js: -------------------------------------------------------------------------------- 1 | import store from '../../../src/plugins/store'; 2 | 3 | import manifest from '../../iiif-api/data/manifests/bl-vdc_00000004216E.json'; 4 | 5 | const app = { config: { globalProperties: {} } }; 6 | 7 | store.install(app, { 8 | manifest: store.convertManifest(manifest), 9 | options: { 10 | language: 'en', 11 | pageLabelFormat: 'P : L', 12 | translationsDirUrl: '', 13 | }, 14 | }); 15 | 16 | const { $store } = app.config.globalProperties; 17 | 18 | // TODO: Add test for getFacingPage 19 | 20 | describe('getStartPages', () => { 21 | it('determines the start page based on startCanvas', () => { 22 | expect($store.getStartPages()).toEqual([7]); 23 | }); 24 | }); 25 | 26 | describe('localize', () => { 27 | it('returns the fallback string if there is no label', () => { 28 | expect($store.localize({})).toEqual(''); 29 | expect($store.localize({ en: '' })).toEqual(''); 30 | expect($store.localize({ en: [] })).toEqual(''); 31 | }); 32 | 33 | it('merges multiple strings unless requested otherwise', () => { 34 | expect($store.localize({ en: ['A', 'B'] })).toEqual('A · B' /* first space:   */); 35 | }); 36 | 37 | it('returns the first label if the set language is not available', () => { 38 | expect($store.localize({ de: 'Beschriftung' })).toEqual('Beschriftung'); 39 | }); 40 | }); 41 | 42 | describe('setPage', () => { 43 | it('sets the page', () => { 44 | expect($store.setPage(1)).toEqual([1]); 45 | expect($store.setPage([0, 1])).toEqual([0, 1]); 46 | expect($store.setPage([1, 3, 5])).toEqual([1, 3, 5]); 47 | }); 48 | 49 | it('throws an error when trying to set an invalid page', () => { 50 | expect(() => $store.setPage('nope')).toThrow(RangeError); 51 | expect(() => $store.setPage(-2)).toThrow(RangeError); 52 | expect(() => $store.setPage(999)).toThrow(RangeError); 53 | expect(() => $store.setPage([5, 3, 1])).toThrow(RangeError); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'; 3 | import viteConfig from './vite.config'; 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | include: ['tests/unit/**/*.spec.js'], 11 | exclude: [...configDefaults.exclude], 12 | globals: true, 13 | root: fileURLToPath(new URL('./', import.meta.url)), 14 | 15 | // Prevent canvas-related error in unit tests 16 | // https://github.com/wobsoriano/vitest-canvas-mock 17 | setupFiles: ['./vitest.setup.js'], 18 | deps: { 19 | optimizer: { 20 | web: { 21 | include: ['vitest-canvas-mock'], 22 | }, 23 | }, 24 | }, 25 | 26 | // Disable output truncation 27 | chaiConfig: { 28 | truncateThreshold: 0, 29 | }, 30 | }, 31 | }), 32 | ); 33 | -------------------------------------------------------------------------------- /vitest.setup.js: -------------------------------------------------------------------------------- 1 | // Prevent canvas-related error in unit tests 2 | // https://github.com/wobsoriano/vitest-canvas-mock 3 | import 'vitest-canvas-mock'; 4 | 5 | // Prevent error "jest is not defined" when using vitest-canvas-mock 6 | // https://github.com/vitest-dev/vitest/issues/2667#issuecomment-1383071037 7 | import { vi } from 'vitest'; 8 | 9 | global.jest = vi; 10 | 11 | // Prevent console.warn from polluting test output 12 | vi.spyOn(console, 'warn').mockImplementation(() => {}); 13 | --------------------------------------------------------------------------------